【问题标题】:Multiple yet mutually exclusive foreign keys - is this the way to go?多个但互斥的外键 - 这是要走的路吗?
【发布时间】:2010-12-02 08:51:16
【问题描述】:

我有三个表:用户、公司和网站。 用户和公司都有网站,因此每个用户记录在网站表中都有一个外键。此外,每条公司记录在网站表中都有一个外键。

现在我想将网站表中的外键重新包含到它们各自的“父”记录中。我怎么做?我是否应该在每个网站记录中有两个外键,其中一个始终为 NULL?还是有别的办法?

【问题讨论】:

    标签: sql mysql database-design data-modeling


    【解决方案1】:

    为什么您需要从网站到用户/公司的外键?不重复数据的原则表明最好扫描用户/公司表以查找匹配的网站 ID。如果您确实需要,您可以始终在网站表中存储一个标志,指示给定网站记录是针对用户还是公司,然后扫描相应的表。

    【讨论】:

      【解决方案2】:

      您不需要父列,您可以在用户和公司表上通过简单的选择(或加入表)来查找父列。如果您想知道这是用户还是公司网站,我建议在您的网站表中使用布尔列。

      【讨论】:

      • +1 - 我说的非常好,所以我清楚地认为你是对的!
      【解决方案3】:

      首先,你真的需要这个双向链接吗?除非绝对需要,否则最好避免使用它。

      我了解到您想知道该网站属于用户还是公司。您可以通过在网站表中添加一个简单的布尔字段 - [BelongsToUser] 来实现。如果为真,则查找用户,如果为假,则查找公司。

      【讨论】:

        【解决方案4】:

        如果我们在这里查看模型,我们将看到以下内容:

        1. 用户只与一个网站相关
          • 一家公司只与一个网站相关
          • 一个网站只与一个用户或公司相关

        第三个关系意味着存在一个“用户或公司”实体,其PRIMARY KEY 应该存储在某个地方。

        要存储它,您需要创建一个表来存储 PRIMARY KEYwebsite owner 实体。该表还可以存储用户和网站共有的属性。

        由于是一对一的关系,所以网站属性也可以存储在这个表中。

        用户和公司不共享的属性应该存储在单独的表中。

        要强制建立正确的关系,您需要将websitePRIMARY KEYowner type 作为其中的一部分,并在具有CHECK 约束的子表中强制使用正确的类型:

        CREATE TABLE website_owner (
            type INT NOT NULL,
            id INT NOT NULL,
            website_attributes,
            common_attributes,
            CHECK (type IN (1, 2)) -- 1 for user, 2 for company
            PRIMARY KEY (type, id)
        )
        
        CREATE TABLE user (
            type INT NOT NULL,
            id INT NOT NULL PRIMARY KEY,
            user_attributes,
            CHECK (type = 1),
            FOREIGN KEY (type, id) REFERENCES website_owner
        )
        
        CREATE TABLE company (
            type INT NOT NULL,
            id INT NOT NULL PRIMARY KEY,
            company_attributes,
            CHECK (type = 2),
            FOREIGN KEY (type, id) REFERENCES website_owner
        )
        

        【讨论】:

        • “使用 CHECK 约束在子表中强制使用正确的类型”——这些在 mySQL 中没有强制执行,对吧?
        • @onedaywhen:不,他们不是。
        【解决方案5】:

        我接受的答案(由 Quassnoi 提出)的问题是对象关系是错误的:公司不是网站所有者的子类型;在我们有网站之前我们有公司,我们可以有网站所有者的公司。此外,在我看来,网站所有权是网站与个人或公司之间的关系,即我们应该在模式中有一个(或两个)关系表。将个人网站所有权与公司网站所有权分开并仅在需要时将它们组合在一起可能是一种可接受的方法,例如通过VIEWs:

        CREATE TABLE People
        (
         person_id CHAR(9) NOT NULL UNIQUE,  -- external identifier
         person_name VARCHAR(100) NOT NULL
        );
        
        CREATE TABLE Companies
        (
         company_id CHAR(6) NOT NULL UNIQUE,  -- external identifier
         company_name VARCHAR(255) NOT NULL
        );
        
        CREATE TABLE Websites
        (
         url CHAR(255) NOT NULL UNIQUE
        );
        
        CREATE TABLE PersonalWebsiteOwnership
        (
         person_id CHAR(9) NOT NULL UNIQUE
            REFERENCES People ( person_id ),
         url CHAR(255) NOT NULL UNIQUE
            REFERENCES Websites ( url )
        );
        
        CREATE TABLE CorporateWebsiteOwnership
        (
         company_id CHAR(6) NOT NULL UNIQUE
            REFERENCES Companies( company_id ),
         url CHAR(255) NOT NULL UNIQUE
            REFERENCES Websites ( url )
        );
        
        CREATE VIEW WebsiteOwnership AS
        SELECT url, company_name AS website_owner_name
          FROM CorporateWebsiteOwnership
               NATURAL JOIN Companies
        UNION
        SELECT url, person_name AS website_owner_name
          FROM PersonalWebsiteOwnership
               NATURAL JOIN People;
        

        上面的问题是没有办法使用数据库约束来强制执行网站由个人或公司所有但不是两者兼有的规则。

        如果我们可以假设 DBMS 强制执行检查约束(正如公认的答案那样),那么我们可以利用(人类)人和公司都是法人的事实并使用超类型表 (LegalPersons)但仍保留关系表方法 (WebsiteOwnership),这次使用 VIEWs 将个人网站所有权与公司网站所有权分开,但这次使用强类型属性:

        CREATE TABLE LegalPersons
        (
         legal_person_id INT NOT NULL UNIQUE,  -- internal artificial identifier
         legal_person_type CHAR(7) NOT NULL
            CHECK ( legal_person_type IN ( 'Company', 'Person' ) ),
         UNIQUE ( legal_person_type, legal_person_id )
        );
        
        CREATE TABLE People
        (
         legal_person_id INT NOT NULL
         legal_person_type CHAR(7) NOT NULL
            CHECK ( legal_person_type = 'Person' ),
         UNIQUE ( legal_person_type, legal_person_id ),
         FOREIGN KEY ( legal_person_type, legal_person_id )
             REFERENCES LegalPersons ( legal_person_type, legal_person_id ),
         person_id CHAR(9) NOT NULL UNIQUE,  -- external identifier
         person_name VARCHAR(100) NOT NULL
        );
        
        CREATE TABLE Companies
        (
         legal_person_id INT NOT NULL
         legal_person_type CHAR(7) NOT NULL
            CHECK ( legal_person_type = 'Company' ),
         UNIQUE ( legal_person_type, legal_person_id ),
         FOREIGN KEY ( legal_person_type, legal_person_id )
             REFERENCES LegalPersons ( legal_person_type, legal_person_id ),
         company_id CHAR(6) NOT NULL UNIQUE,  -- external identifier
         company_name VARCHAR(255) NOT NULL
        );
        
        CREATE TABLE WebsiteOwnership
        (
         legal_person_id INT NOT NULL
         legal_person_type CHAR(7) NOT NULL
         UNIQUE ( legal_person_type, legal_person_id ),
         FOREIGN KEY ( legal_person_type, legal_person_id )
             REFERENCES LegalPersons ( legal_person_type, legal_person_id ),
         url CHAR(255) NOT NULL UNIQUE
            REFERENCES Websites ( url )
        );
        
        CREATE VIEW CorporateWebsiteOwnership AS 
        SELECT url, company_name
          FROM WebsiteOwnership
               NATURAL JOIN Companies;
        
        CREATE VIEW PersonalWebsiteOwnership AS
        SELECT url, person_name
          FROM WebsiteOwnership
               NATURAL JOIN Persons;
        

        我们需要的是新的 DBMS 功能,用于“分布式外键”(“对于此表中的每一行,其中一个表中必须恰好有一行”)和“多重赋值”以允许将数据添加到因此,表被限制在单个 SQL 语句中。遗憾的是,我们距离获得这些功能还有很长的路要走!

        【讨论】:

          【解决方案6】:

          有点晚了,但所有现有的答案似乎都没有达到标准:

          • 网站所有者是1:Many 关系
          • 网站所有者是1:1 关系
          • Users 和 Companies 表不应有外键进入 Websites 表
          • 任何网站数据(无论是否为用户和公司共有)都不应包含在“用户”或“公司”表中
          • 所有者的任何信息(无论是否常见)都不应出现在网站表中
          • MySQL 会默默地忽略 CHECK 对表的约束(不强制执行引用完整性)
          • DBMS 应该处理“关系”逻辑,而不是使用数据库的应用程序

          其中一些在 answer from onedaywhen 中得到认可,但该答案仍然错过了让 MySQL 完成繁重工作并强制执行引用完整性的机会。


          无论如何,一个网站在法律上只能有一个所有者。一个人或公司可以拥有任意数量的网站,包括没有。数据库中从所有者到网站的链接在任何标准化级别只能是1:1。实际上,该关系是1:Many,并且需要为碰巧拥有多个网站的每个所有者拥有多个表条目。从网站到所有者的链接在数据库术语和现实中都是1:1。拥有从网站到所有者的链接更好地代表了模型。通过网站表中的索引,对给定所有者进行1:Many 查找变得相当有效。

          SQL 中的CHECK 属性将是一个很好的解决方案,如果 MySQL 没有碰巧忽略它的话。

          MySQL 文档13.1.20 CREATE TABLE Syntax

          CHECK 子句被解析,但被所有存储引擎忽略。

          MySQL 的功能确实提供了两种解决方案作为变通方法来实现CHECK 的行为并保持数据的引用完整性。带有存储过程的触发器就是其中之一,它适用于各种约束。使用VIEWWITH CHECK OPTION 子句更容易实现,尽管通用性较差,MySQL 实现。

          MySQL 文档24.5.4 The View WITH CHECK OPTION Clause

          WITH CHECK OPTION 子句可用于可更新视图,以防止插入 select_statement 中的 WHERE 子句不正确的行。它还可以防止更新WHERE 子句为真但更新会导致它不为真的行(换句话说,它会阻止可见行被更新为不可见行)。

          MySQLTUTORIAL 网站在他们的Introduction to the SQL CHECK constraint 教程中给出了这两个选项的一个很好的例子。 (您必须考虑错别字,否则很好。)


          在尝试解决类似的互斥外键拆分并开发解决方案时发现了这个问题,并通过答案生成了提示,似乎只适合作为回报分享我的解决方案。

          推荐解决方案

          为了尽量减少对现有架构和访问数据的应用程序的影响,请保留 UsersCompanies 表的原样。重命名Websites 表并将其替换为应用程序可以继续访问的名为Websites 的视图。除了在处理所有权信息时,对Websites 的所有旧查询应该仍然有效。所以:

          设置

          -- Keep the `Users` table about "users"
          CREATE TABLE `Users` (
              `id` INT SERIAL PRIMARY KEY,
              `name` VARCHAR(180),
              -- user_attributes
          );
          
          -- Keep the `Companies` table about "companies"
          CREATE TABLE `Companies` (
              `id` SERIAL PRIMARY KEY,
              `name` VARCHAR(180),
              -- company_attributes
          );
          
          -- Attach ownership information about the website to the website's record in the `Websites` table, renamed to `WebsitesData`
          CREATE TABLE `WebsitesData` (
              `id` SERIAL PRIMARY KEY,
              `name` VARCHAR(255),
              `is_personal` BOOL,
              `owner_user` BIGINT UNSIGNED DEFAULT NULL,
              `owner_company` BIGINT UNSIGNED DEFAULT NULL,
              website_attributes,
              FOREIGN KEY `WebsiteOwner_User` (`owner_user`)
                  REFERENCES `Users` (`id`)
                      ON DELETE RESTRICT ON UPDATE CASCADE,
              FOREIGN KEY `WebsiteOwner_Company` (`owner_company`)
                  REFERENCES `Companies` (`id`)
                      ON DELETE RESTRICT ON UPDATE CASCADE,
          );
          
          -- Create a new `VIEW` with the original name of `Websites` as the gateway to the website records which can enforce the constraints you need
          CREATE VIEW `Websites` AS
          SELECT * FROM `WebsitesData` WHERE
              (`is_personal`=TRUE AND `owner_user` IS NOT NULL AND `owner_company` IS NULL) OR
              (`is_personal`=FALSE AND `owner_user` IS NULL AND `owner_company` IS NOT NULL)
          WITH CHECK OPTION;
          

          用法

          -- Use the Websites VIEW for the INSERT, UPDATE, and SELECT operations as you normally would and leave the WebsitesData table in the background.
          INSERT INTO `Websites` SET
              `is_personal`=TRUE,
              `owner_user`=$userID;
          INSERT INTO `Websites` SET
              `is_personal`=FALSE,
              `owner_company`=$companyID;
          
          -- Or, using different field lists based on the type of owner
          INSERT INTO `Websites` (`is_personal`,`owner_user`, ...)
              VALUES (TRUE, $userID, ...);
          INSERT INTO `Websites` (`is_personal`,`owner_company`, ...)
              VALUES (FALSE, $companyID, ...);
          
          -- Or, using a common field list, and placing NULL in the proper place
          INSERT INTO `Websites` (`is_personal`,`owner_user`,`owner_company`,...)
              VALUES (TRUE, $userID, NULL, ...);
          INSERT INTO `Websites` (`is_personal`,`owner_user`,`owner_company`,...)
              VALUES (FALSE, NULL, $companyID, ...);
          
          -- Change the company that owns a website
          -- Will ERROR if the site was owned by a User.
          UPDATE `Websites` SET `owner_company`=$new_companyID;
          
          -- Force change the ownership from a User to a Company
          UPDATE `Websites` SET
              `owner_company`=$new_companyID,
              `owner_user`=NULL,
              `is_personal`=FALSE;
          
          -- Force change the ownership from a Company to a User
          UPDATE `Websites` SET
              `owner_user`=$new_userID,
              `owner_company`=NULL,
              `is_personal`=TRUE;
          
          -- Selecting the owner of a site without needing to know if it is personal or not
          (SELECT `Users`.`name` AS `Owner`
              FROM `Websites`
                  JOIN `Users` ON `Websites`.`owner_user`=`Users`.`id`
              WHERE `is_personal`=TRUE AND `Websites`.`id`=$siteID)
          UNION
          (SELECT `Companies`.`name` AS `Owner`
              FROM `Websites`
                  JOIN `Companies` ON `Websites`.`owner_company`=`Companies`.`id`
              WHERE `is_personal`=FALSE AND `Websites`.`id`=$siteID);
          
          -- Selecting the sites owned by a User
          SELECT `name` FROM `Websites`
              WHERE `is_personal`=TRUE AND `id`=$userID;
          SELECT `Websites`.`name`
              FROM `Websites`
                  JOIN `Users` ON `Websites`.`owner_user`=`Users`.$userID
              WHERE `is_personal`=TRUE AND `Users`.`name`="$user_name";
          
          -- Selecting the sites owned by a Company
          SELECT `name` FROM `Websites` WHERE `is_personal`=FALSE AND `id`=$companyID;
          SELECT `Websites`.`name`
              FROM `Websites`
                  JOIN `Comnpanies` ON `Websites`.`owner_company`=`Companies`.$userID
              WHERE `is_personal`=FALSE AND `Companies`.`name`="$company_name";
          
          -- Listing all websites and their owners
          (SELECT `Websites`.`name` AS `Website`,`Users`.`name` AS `Owner`
              FROM `Websites`
                  JOIN `Users` ON `Websites`.`owner_user`=`Users`.`id`
              WHERE `is_personal`=TRUE)
          UNION ALL
          (SELECT `Websites`.`name` AS `Website`,`Companies`.`name` AS `Owner`
              FROM `Websites`
                  JOIN `Companies` ON `Websites`.`owner_company`=`Companies`.`id`
              WHERE `is_personal`=FALSE)
          ORDER BY Website, Owner;
          
          -- Listing all users or companies which own at least one website
          (SELECT `Websites`.`name` AS `Website`,`Users`.`name` AS `Owner`
              FROM `Websites`
                  JOIN `Users` ON `Websites`.`owner_user`=`Users`.`id`
              WHERE `is_personal`=TRUE)
          UNION DISTINCT
          (SELECT `Websites`.`name` AS `Website`,`Companies`.`name` AS `Owner`
              FROM `Websites`
                  JOIN `Companies` ON `Websites`.`owner_company`=`Companies`.`id`
              WHERE `is_personal`=FALSE)
          GROUP BY `Owner` ORDER BY `Owner`;
          

          标准化水平提升

          作为规范化的技术说明,所有权信息可以从网站表中提取出来,并创建一个新表来保存所有权数据,包括 is_normal 列。

          CREATE TABLE `Websites` (
              `id` SERIAL PRIMARY KEY,
              `name` VARCHAR(255),
              `owner` BIGINT UNSIGNED DEFAULT NULL,
              website_attributes,
              FOREIGN KEY `Website_Owner` (`owner`)
                  REFERENCES `WebOwners` (id`)
                      ON DELETE RESTRICT ON UPDATE CASCADE
          );
          
          CREATE TABLE `WebOwnersData` (
              `id` SERIAL PRIMARY KEY,
              `is_personal` BOOL,
              `user` BIGINT UNSIGNED DEFAULT NULL,
              `company` BIGINT UNSIGNED DEFAULT NULL,
              FOREIGN KEY `WebOwners_User` (`user`)
                  REFERENCES `Users` (`id`)
                      ON DELETE RESTRICT ON UPDATE CASCADE,
              FOREIGN KEY `WebOwners_Company` (`company`)
                  REFERENCES `Companies` (`id`)
                      ON DELETE RESTRICT ON UPDATE CASCADE,
          );
          
          CREATE VIEW `WebOwners` AS
          SELECT * FROM WebsitesData WHERE
              (`is_personal`=TRUE AND `user` IS NOT NULL AND `company` IS NULL) OR
              (`is_personal`=FALSE AND `user` IS NULL AND `company` IS NOT NULL)
          WITH CHECK OPTION;
          

          但是,我相信,创建的 VIEW 及其约束可以防止规范化旨在消除的任何异常,并增加这种情况下不需要的复杂性。无论如何,标准化过程总是需要权衡取舍。

          【讨论】:

          • 嗨,这个解决方案从规范化和概念的角度来看是不正确的。您添加了在业务环境中没有意义的额外列,我认为即使在您的解决方案中也是多余的。而且你有这么多空外键(当数据库逐渐增长时)。如果我们在一张表中有 4 或 5 个互斥外键怎么办?
          • @Arash 不确定哪一列在业务上下文中没有意义,据我当时所知,没有办法让 MySQL 在不诉诸存储过程的情况下强制执行互斥外键排列,即使那样。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-12-14
          • 1970-01-01
          • 2015-07-19
          • 1970-01-01
          相关资源
          最近更新 更多