简体   繁体   English

多个但相互排斥的外键 - 这是要走的路吗?

[英]Multiple yet mutually exclusive foreign keys - is this the way to go?

I have three tables: Users, Companies and Websites. 我有三个表:用户,公司和网站。 Users and companies have websites, and thus each user record has a foreign key into the Websites table. 用户和公司拥有网站,因此每个用户记录都有一个外键进入网站表。 Also, each company record has a foreign key into the Websites table. 此外,每个公司记录都有一个外键进入网站表。

Now I want to include foreign keys in the Websites table back into their respective "parent" records. 现在我想将网站表中的外键包含回各自的“父”记录中。 How do I do that? 我怎么做? Should I have two foreign keys in each website record, with one of them always NULL? 我应该在每个网站记录中有两个外键,其中一个永远是NULL吗? Or is there another way to go? 或者还有另一种方法吗?

If we look into the model here, we will see the following: 如果我们在这里查看模型,我们将看到以下内容:

  1. A user is related to exactly one website 用户与一个网站完全相关
    • A company is related to exactly one website 公司只与一个网站相关
    • A website is related to exactly one user or company 网站只与一个用户或公司相关

The third relation implies existence of a "user or company" entity whose PRIMARY KEY should be stored somewhere. 第三种关系意味着存在“用户或公司”实体,其PRIMARY KEY应存储在某处。

To store it you need to create a table that would store a PRIMARY KEY of a website owner entity. 要存储它,您需要创建一个表,用于存储website owner实体的PRIMARY KEY This table can also store attributes common for a user and a website. 该表还可以存储用户和网站共有的属性。

Since it's a one-to-one relation, website attributes can be stored in this table too. 由于它是一对一的关系,因此网站属性也可以存储在此表中。

The attributes not shared by users and companies should be stored in the separate table. 用户和公司未共享的属性应存储在单独的表中。

To force the correct relationships, you need to make the PRIMARY KEY of the website composite with owner type as a part of it, and force the correct type in the child tables with a CHECK constraint: 要强制建立正确的关系,您需要使用owner type作为其一部分来构建website复合的PRIMARY KEY ,并使用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
)

you don't need a parent column, you can lookup the parents with a simple select (or join the tables) on the users and companies table. 如果您不需要父列,则可以通过在users和companies表上进行简单的select(或连接表)来查找父级。 if you want to know if this is a user or a company website i suggest using a boolean column in your websites table. 如果您想知道这是用户还是公司网站,我建议您在网站表格中使用布尔列。

Why do you need a foreign key from website to user/company at all? 为什么你需要从网站到用户/公司的外键? The principle of not duplicating data would suggest it might be better to scan the user/company tables for a matching website id. 不重复数据的原则表明,扫描用户/公司表以获得匹配的网站ID可能更好。 If you really need to you could always store a flag in the website table that denotes whether a given website record is for a user or a company, and then scan the appropriate table. 如果您真的需要,您可以随时在网站表中存储一个标志,表示给定的网站记录是针对用户还是公司,然后扫描相应的表格。

The problem I have with the accepted answer (by Quassnoi) is that the object relationships are the wrong way around: company is not a sub-type of a website owner; 我接受的答案(Quassnoi)的问题是对象关系是错误的方式:公司不是网站所有者的子类型; we had companies before we had websites and we can have companies who are website owners. 在我们拥有网站之前我们有公司,我们可以拥有网站所有者的公司。 Also, it seems to me that website ownership is a relationship between a website and either a person or a company ie we should have a relationship table (or two) in the schema. 此外,在我看来,网站所有权是网站与个人或公司之间的关系,即我们应该在模式中有一个关系表(或两个)。 It may be an acceptable approach to keep personal website ownership separate from corporate website ownership and only bring them together when required eg via VIEW s: 将个人网站所有权与公司网站所有权分开是一种可接受的方法,只有在需要时才将它们组合在一起,例如通过VIEW

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;

The problem with the above is there is no way of using database constraints to enforce the rule that a website is either owned by a person or a company but not both. 上述问题是没有办法使用数据库约束来强制执行网站由个人或公司拥有但不是两者都拥有的规则。

If we can assuming the DBMS enforces check constraints (as the accepted answer does) then we can exploit the fact that a (human) person and a company are both legal persons and employ a super-type table ( LegalPersons ) but still retain relationship table approach ( WebsiteOwnership ), this time using the VIEW s to separate personal website ownership from separate from corporate website ownership but this time with strongly typed attributes: 如果我们可以假设DBMS强制执行检查约束(如接受的答案那样),那么我们可以利用这样一个事实:(人)和公司都是合法人员并使用超类型表( LegalPersons )但仍保留关系表方法( WebsiteOwnership ),这次使用VIEW将个人网站所有权与公司网站所有权分开,但这次使用强类型属性:

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;

What we need are new DBMS features for 'distributed foreign keys' ("For each row in this table there must be exactly one row in one of these tables") and 'multiple assignment' to allow the data to be added into tables thus constrained in a single SQL statement. 我们需要的是“分布式外键”的新DBMS功能(“对于此表中的每一行,其中一个表中必须只有一行”)和“多重赋值”,以允许将数据添加到表中,从而限制在单个SQL语句中。 Sadly we are a far way from getting such features! 可悲的是,我们距离获得这些功能还有很长的路要走!

First of all, do you really need this bi-directional link? 首先,你真的需要这种双向链接吗? It is a good practice to avoid it unless absolutely needed. 除非绝对需要,否则最好避免使用它。

I understand it that you wish to know whether the site belongs to a user or to a company. 我理解您希望了解该网站是属于用户还是属于公司。 You can achieve that by having a simple boolean field in the Website table - [BelongsToUser]. 你可以通过在Website表中有一个简单的布尔字段来实现这一点 - [BelongsToUser]。 If true, then you look up a user, if false - you look up a company. 如果是,那么你查找一个用户,如果错误 - 你查找公司。

A bit late, but all the existing answers seemed to fall somewhat short of the mark: 有点晚了,但所有现有的答案似乎都没有达到标准:

  • Owner to website is a 1:Many relation 网站所有者是1:Many关系
  • Website to owner is a 1:1 relation 所有者的网站是1:1关系
  • Users and Companies tables should not have a foreign key into the Websites table Users和Companies表不应在Websites表中具有外键
  • None of the website data, common to users and companies or not, should be in the Users or Companies tables 用户和公司共有的网站数据都不应位于“用户”或“公司”表中
  • None of the owner's information, common or not, should be in the Websites table 所有者的信息,无论是否共同,都不应该在网站表中
  • MySQL ignores, silently, CHECK constraints on tables (no enforcement of referential integrity) MySQL默认忽略对表的CHECK约束(不强制引用完整性)
  • The DBMS ought to handle the 'relation' logic, not the application using the database DBMS应该处理“关系”逻辑,而不是使用数据库的应用程序

Some of this is recognized in the answer from onedaywhen , yet that answer still missed the opportunity to make MySQL do the heavy lifting and enforce the referential integrity. 其中一些在onedaywhen答案中得到了认可 ,但是这个答案仍然错过了让MySQL做繁重并强制执行参照完整性的机会。


A website can only have one owner, legally, anyway. 无论如何,一个网站只能合法拥有一个所有者。 A person, or company, can have any number of websites, including none. 一个人或公司可以拥有任意数量的网站,包括没有网站。 A link in the database from owner to website can only be 1:1 at any level of normalization. 从所有者到网站的数据库中的链接在任何标准化级别上只能是1:1 In reality the relation is 1:Many , and would require having multiple table entries for each owner that happens to own more than one website. 实际上,关系是1:Many ,并且需要为每个拥有多个网站的所有者提供多个表条目。 A link from website to owner is 1:1 in both database terms and in reality. 从网站到所有者的链接在数据库术语和实际上都是1:1 Having the link from website to owner represents the model better. 拥有从网站到所有者的链接更好地代表了模型。 With an index in the website table, doing the 1:Many lookup for a given owner becomes reasonably efficient. 使用网站表中的索引,执行1:Many对给定所有者的多次查找变得相当有效。

The CHECK attribute in SQL would be an excellent solution, if MySQL didn't happen to silently ignore it. SQL中的CHECK属性将是一个很好的解决方案,如果MySQL没有发生静默忽略它。

MySQL Docs 13.1.20 CREATE TABLE Syntax MySQL Docs 13.1.20 CREATE TABLE语法

The CHECK clause is parsed but ignored by all storage engines. CHECK子句被解析但被所有存储引擎忽略。

MySQL's functionality does offer two solutions as work-arounds to implement the behavior of CHECK and keep the referential integrity of the data. MySQL的功能提供了两种解决方案作为解决方案来实现CHECK的行为并保持数据的引用完整性。 Triggers with stored procedures is one, and works well with all manner of constraints. 存储过程的触发器是一个,并且适用于所有类型的约束。 Easier to implement, though less versatile, is using a VIEW with a WITH CHECK OPTION clause, which MySQL will implement. 更容易实现,虽然功能较少,但是使用带有WITH CHECK OPTION子句的VIEW ,MySQL 实现该子句。

MySQL Docs 24.5.4 The View WITH CHECK OPTION Clause MySQL Docs 24.5.4视图WITH CHECK OPTION子句

The WITH CHECK OPTION clause can be given for an updatable view to prevent inserts to rows for which the WHERE clause in the select_statement is not true. 可以为可更新视图提供WITH CHECK OPTION子句,以防止插入到select_statement WHERE子句不为true的行。 It also prevents updates to rows for which the WHERE clause is true but the update would cause it to be not true (in other words, it prevents visible rows from being updated to nonvisible rows). 它还会阻止更新WHERE子句为true的行,但更新会导致它不为true(换句话说,它会阻止可见行更新为不可见的行)。

The MySQLTUTORIAL site gives a good example of both options in their Introduction to the SQL CHECK constraint tutorial. MySQLTUTORIAL站点在他们的SQL CHECK约束教程简介中给出了两个选项的一个很好的例子。 (You have to think around the typos, but good otherwise.) (你必须考虑拼写错误,但不然。)


Having found this question while trying to resolve a similar mutually exclusive foreign key split and developing a solution, with hints generated by the answers, it seems only proper to share my solution in return. 在尝试解决类似的互斥外键拆分并开发解决方案时发现了这个问题,并且通过答案生成了提示,似乎只有合适的方式才能分享我的解决方案。

Recommended Solution 推荐解决方案

For the minimum impact to the existing schema, and the application accessing the data, retain the Users and Companies tables as they are. 为了对现有模式和访问数据的应用程序产生最小影响,请保留UsersCompanies表。 Rename the Websites table and replace it with a VIEW named Websites which the application can continue to access. 重命名Websites表并将其替换为应用程序可以继续访问的名为Websites的VIEW。 Except when dealing with the ownership information, all the old queries to Websites should still work. 除了处理所有权信息之外,对Websites所有旧查询仍应有效。 So: 所以:

The setup 设置

-- 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;

Usage 用法

-- 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`;

Normalization Level Up 标准化水平提高

As a technical note for normalization, the ownership information could be factored out of the Websites table and a new table created to hold the ownership data, including the is_normal column. 作为规范化的技术说明,可以从网站表中分解所有权信息,并创建一个新表来保存所有权数据,包括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;

I believe, however, that the created VIEW, with its constraints, prevents any of the anomalies that normalization aims to remove, and adds complexity that is not needed in the situation. 但是,我相信,创建的VIEW及其约束可以防止规范化旨在消除的任何异常,并增加情境中不需要的复杂性。 The normalization process is always a trade off anyway. 无论如何,规范化过程始终是一种权衡。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM