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

发布于 2024-08-06 07:34:42 字数 176 浏览 6 评论 0原文

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

现在我想将 Websites 表中的外键包含回其各自的“父”记录中。我该怎么做?我是否应该在每个网站记录中有两个外键,其中之一始终为 NULL?或者还有别的路可走吗?

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? Or is there another way to go?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

不美如何 2024-08-13 07:34:42

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

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

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

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

由于它是一对一的关系,因此网站属性也可以存储在该表中。

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

要强制建立正确的关系,您需要将 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
)

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.

To store it you need to create a table that would store a PRIMARY KEY of a website owner entity. 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:

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
)
薄凉少年不暖心 2024-08-13 07:34:42

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

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. if you want to know if this is a user or a company website i suggest using a boolean column in your websites table.

夜司空 2024-08-13 07:34:42

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

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

如日中天 2024-08-13 07:34:42

我对接受的答案(Quassnoi)的问题是,对象关系是错误的:公司不是网站所有者的子类型;公司不是网站所有者的子类型。在拥有网站之前我们就已经有了公司,而且我们可以拥有作为网站所有者的公司。另外,在我看来,网站所有权是网站与个人或公司之间的关系,即我们应该在架构中拥有一个(或两个)关系表。将个人网站所有权与公司网站所有权分开并仅在需要时将它们放在一起(例如通过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;

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

如果我们可以假设 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;

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

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; 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 i.e. 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 e.g. via 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;

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 VIEWs to separate personal website ownership from separate from corporate website ownership but this time with strongly typed attributes:

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. Sadly we are a far way from getting such features!

怪我入戏太深 2024-08-13 07:34:42

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

据我了解,您想知道该网站是属于用户还是属于公司。您可以通过在 Website 表中添加一个简单的布尔字段 - [BelongsToUser] 来实现这一点。如果为 true,那么您会查找用户,如果为 false,您会查找一家公司。

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]. If true, then you look up a user, if false - you look up a company.

羞稚 2024-08-13 07:34:42

有点晚了,但所有现有的答案似乎都有些达不到目标:

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

其中一些在 < a href="https://stackoverflow.com/a/40218845/7412956" title="Other Answer">来自 onedaywhen 的答案,但该答案仍然错过了让 MySQL 承担繁重工作并强制执行的机会参照完整性。


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

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

MySQL 文档 13.1.20创建表语法

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

MySQL 的功能确实提供了两种解决方案作为解决方案来实现 CHECK 行为并保持数据的引用完整性。带有存储过程的触发器就是其中之一,并且可以很好地适应各种约束。虽然通用性较差,但更容易实现的是使用 VIEWWITH CHECK OPTION 子句,MySQL 将实现

MySQL 文档 24.5 .4 视图WITH CHECK OPTION子句

可以为可更新视图指定 WITH CHECK OPTION 子句,以防止插入到 select_statementWHERE 子句的行代码> 不正确。它还会阻止更新 WHERE 子句为 true 的行,但更新会导致其不为 true(换句话说,它会阻止可见行更新为不可见行)。

MySQLTUTORIAL 站点在其 SQL 简介中提供了这两个选项的一个很好的示例检查约束教程。 (您必须考虑拼写错误,但否则很好。)


在尝试解决类似的互斥外键拆分并开发解决方案时发现了这个问题,并根据答案生成了提示,似乎只适合在以下位置分享我的解决方案返回。

推荐的解决方案

为了将对现有架构和访问数据的应用程序的影响降至最低,请按原样保留 UsersCompanies 表。重命名 Websites 表并将其替换为应用程序可以继续访问的名为 Websites 的 VIEW。除了处理所有权信息外,所有对 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`;

标准化级别

作为标准化的技术说明,可以从 Websites 表中提取所有权信息,并创建一个新表来保存所有权数据,包括 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 及其限制可以防止规范化旨在消除的任何异常情况,并增加情况下不需要的复杂性。无论如何,标准化过程始终是一种权衡。

A bit late, but all the existing answers seemed to fall somewhat short of the mark:

  • Owner to website is a 1:Many relation
  • Website to owner is a 1:1 relation
  • Users and Companies tables should not have a foreign key into the Websites table
  • 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)
  • The DBMS ought to handle the 'relation' logic, not the application using the database

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.


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. 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. A link from website to owner is 1:1 in both database terms and in reality. 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.

The CHECK attribute in SQL would be an excellent solution, if MySQL didn't happen to silently ignore it.

MySQL Docs 13.1.20 CREATE TABLE Syntax

The CHECK clause is parsed but ignored by all storage engines.

MySQL's functionality does offer two solutions as work-arounds to implement the behavior of CHECK and keep the referential integrity of the data. 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.

MySQL Docs 24.5.4 The View WITH CHECK OPTION Clause

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

The MySQLTUTORIAL site gives a good example of both options in their Introduction to the SQL CHECK constraint tutorial. (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. Rename the Websites table and replace it with a VIEW named Websites which the application can continue to access. Except when dealing with the ownership information, all the old queries to Websites should still work. 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.

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. The normalization process is always a trade off anyway.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文