在单个列中使用来自多个表的 ID
Posted
技术标签:
【中文标题】在单个列中使用来自多个表的 ID【英文标题】:Using IDs from multiple tables in a single column 【发布时间】:2011-10-19 05:48:27 【问题描述】:我的一位同事创建了一个类似于以下的架构。这是一个简化的架构,仅包括解决此问题所需的部分。
系统规则如下:
-
部门可以有 0 到多个部门。
一个部门只能属于一个部门。
一篇文章可以分配给一个部门或该部门的一个部门。
架构是:
Department
----------
DepartmentID (PK) int NOT NULL
DepartmentName varchar(50) NOT NULL
Division
--------
DivisionID (PK) int NOT NULL
DepartmentID (FK) int NOT NULL
DivisonName varchar(50) NOT NULL
Article
-------
ArticleID (PK) int NOT NULL
UniqueID int NOT NULL
ArticleName varchar(50) NOT NULL
他使用虚构的规则(因为没有更好的术语)定义了架构,所有 DepartmentID 都在 1 到 100 之间,所有 DivisionID 都在 101 到 200 之间。他指出,在查询 Article 表时,您将根据其所属的范围知道 UniqueID 是来自 Department 表还是 Division 表。
我认为这是一个糟糕的设计,并提出了以下替代架构:
Department
----------
DepartmentID (PK) int NOT NULL
ParentDepartmentID (FK) int NULL /* Self-referencing foreign key. Divisions have parent departments. */
DepartmentName varchar(50) NOT NULL
Article
-------
ArticleID (PK) int NOT NULL
DepartmentID (FK) int NOT NULL
ArticleName varchar(50) NOT NULL
我相信这是一个适当规范化的架构,可以适当地执行关系和数据完整性,同时遵守上述业务规则。
我的具体问题是这样的:
我知道使用一列来包含来自两个域的值是糟糕的设计,我可以在 Article 表中争论外键的好处。但是,有人可以提供对特定数据库设计文章/论文的参考,我可以用它来备份我的位置。如果我能指出一些具体的东西,它会变得容易得多。
【问题讨论】:
我非常感谢下面介绍的细节、辩论和替代解决方案。我对任何我认为对我的问题有洞察力的答案都投了赞成票。但是,我给了 Damir 的答案,因为我觉得他的回答直接解决了具体问题。 【参考方案1】:您的同事实施了一个名为多态关联的设计。也就是说,“外键”指的是两个不同的父表之一。大多数人添加另一列parent_type
或类似的东西,以便您可以知道给定行引用哪个父表。在您的同事的情况下,他反而细分了 id 的范围。这是一个脆弱的设计,因为您无法在数据库级别强制执行它。如果您插入的部门编号 > 100,您将无法知道您的文章是否适用于部门或部门。
鉴于您开发了一个类似于Single Table Inheritance 的设计,您将多个相关类型存储在一个表中,因此主键可以确保保持唯一,并且文章可以引用任何相关类型的任何实例.
这是另一种选择:
想想面向对象的设计。如果你想让两个不同的类有文章,你可以为这两个类创建一个公共超类或一个公共接口。你可以在 SQL 中做同样的事情:
ArticleProducer
---------------
ProducerID (PK) int NOT NULL
Department
----------
DepartmentID (PK) int NOT NULL, (FK)->ArticleProducer
DepartmentName varchar(50) NOT NULL
Division
--------
DivisionID (PK) int NOT NULL, (FK)->ArticleProducer
DepartmentID (FK) int NOT NULL
DivisonName varchar(50) NOT NULL
Article
-------
ArticleID (PK) int NOT NULL, (FK)->ArticleProducer
UniqueID int NOT NULL
ArticleName varchar(50) NOT NULL
所以一篇文章必须由单个ArticleProducer
制作。每个部门或部门都是 ArticleProducer。
另见Why can you not have a foreign key in a polymorphic association?
有关多态关联的更多信息,请参阅我的演示文稿Practical Object-Oriented Models in SQL,或我的书,SQL Antipatterns: Avoiding the Pitfalls of Database Programming。
来自 Erwin Smout 的回复:
你是对的,试图从所有子类型表中强制执行不超过 一个 行有点棘手。不幸的是,mysql 不支持任何存储引擎中的 CHECK 约束。您可以使用查找表实现类似的功能:
CREATE TABLE ArticleProducerTypes (ProducerType TINYINT UNSIGNED PRIMARY KEY);
INSERT INTO ArticleProducerTypes VALUES (1), (2);
CREATE TABLE ArticleProducer (
ProducerID INT UNSIGNED NOT NULL PRIMARY KEY,
ProducerType TINYINT UNSIGNED NOT NULL,
UNIQUE KEY (ProducerID,ProducerType),
FOREIGN KEY (ProducerType)
REFERENCES ArticleProducerTypes(ProducerType)
) ENGINE=InnoDB;
CREATE TABLE DepartmentProducerType (ProducerType TINYINT UNSIGNED PRIMARY KEY);
INSERT INTO DepartmentProducerType VALUES (1);
CREATE TABLE Department (
DepartmentID INT UNSIGNED NOT NULL PRIMARY KEY,
DepartmentName VARCHAR(50) NOT NULL,
ProducerType TINYINT UNSIGNED NOT NULL,
FOREIGN KEY (DepartmentID, ProducerType)
REFERENCES ArticleProducer(ProducerID, ProducerType),
FOREIGN KEY (ProducerType)
REFERENCES DepartmentProducerType(ProducerType) -- restricted to '1'
) ENGINE=InnODB;
CREATE TABLE DivisionProducerType (ProducerType TINYINT UNSIGNED PRIMARY KEY);
INSERT INTO DivisionProducerType VALUES (2);
CREATE TABLE Division (
DivisionID INT UNSIGNED NOT NULL PRIMARY KEY,
ProducerType TINYINT UNSIGNED NOT NULL,
DepartmentID INT UNSIGNED NOT NULL,
FOREIGN KEY (DivisionID, ProducerType)
REFERENCES ArticleProducer(ProducerID, ProducerType),
FOREIGN KEY (ProducerType)
REFERENCES DivisionProducerType(ProducerType), -- restricted to '2'
FOREIGN KEY (DepartmentID)
REFERENCES Department(DepartmentID)
) ENGINE=InnODB;
CREATE TABLE Article (
ArticleID INT UNSIGNED NOT NULL PRIMARY KEY,
ArticleName VARCHAR(50) NOT NULL,
FOREIGN KEY (ArticleID)
REFERENCES ArticleProducer(ProducerID)
);
现在 ArticleProducer 中的每个给定行都可以被 Department 或 Division 引用,但不能同时被两者引用。
如果我们想添加一个新的生产者类型,我们在 ArticleProducerTypes 查找表中添加一行,并为新类型创建一对新表。例如:
INSERT INTO ArticleProducerTypes VALUES (3);
CREATE TABLE PartnerProducerType (ProducerType TINYINT UNSIGNED PRIMARY KEY);
INSERT INTO PartnerProducerType VALUES (3);
CREATE TABLE Partner (
PartnerID INT UNSIGNED NOT NULL PRIMARY KEY,
ProducerType TINYINT UNSIGNED NOT NULL,
FOREIGN KEY (PartnerID, ProducerType)
REFERENCES ArticleProducer(ProducerID, ProducerType),
FOREIGN KEY (ProducerType)
REFERENCES PartnerProducerType(ProducerType) -- restricted to '3'
) ENGINE=InnODB;
但我们仍然有可能 包含对 ArticleProducer 中给定行的引用;即我们不能做出强制在其中一个依赖表中创建行的约束。我对此没有解决方案。
【讨论】:
articleProducer 中包含 producerName 不是比每个 producer 表中更好吗? @Beth,当然,将列放入所有子类型共有的生产者表中是很常见的。然后你正在实现 Martin Fowler 的类表继承模式。 “在你的同事的例子中,他已经细分了 id 的范围。这是一个脆弱的设计,因为你无法在数据库级别强制执行它。”你真的这么说吗,比尔? CHECK 约束足以强制执行该细分。不能以声明方式强制执行的是 FK,其大意是所有文章都必须属于某个东西(部门或部门)。这可能是你想说的,但不是你说的。 您的替代“解决方案”仍然缺少 DepartmentID 集和 DivisionID 集必须不相交的约束。如果不这样做,您仍然可能会在某些 ArticleProduces 是部门还是部门方面存在歧义。【参考方案2】:1NF
每个行列交叉点都只包含一个来自 适用域(仅此而已)。
http://en.wikipedia.org/wiki/First_normal_form#1NF_tables_as_representations_of_relations
解决您的问题最简单的方法是为每个部门引入“默认”划分,即“整个部门”。之后,只需将所有文章链接到部门。
也许是这样的(DepartmentDivisionNo = 0
表示整个部门):
【讨论】:
感谢您的链接,我确实将您提出的架构视为另一种选择。但是,为什么不只使用 DivisionID 而不是 DepartmentDivisionNo 并让文章与部门相关?然后,从文章到部门将只有一个 FK。如果我必须回到部门,我可以再加入一次。 @NYSystemsAnalyst;是的,那也行。您确实需要一些东西来防止在同一部门内重复划分,因此需要在Division
上建立一个额外的索引。这样您就可以直接加入Article
到Department
。无论如何,你的喜好。也看到这个***.com/questions/4520289/…
如果部门不是部门,部门也不是部门,那么让数据库假装每个部门都存在一个实际上不存在的部门是一个糟糕的设计,因为它代表代表“整个部门”。【参考方案3】:
我其实很喜欢 Damir 的回答——它“重新思考”了这个问题,并为这个新问题提供了正确的答案。然而,部门和部门之间是有区别的——大概每个部门都可以访问属于他们部门的文章。拥有属于默认或整个部门部门的文章意味着有两种不同类型的部门。从现在开始,您将进行诸如
之类的查询select * from xxx x inner join division d where d.joinkey = x.joinkey and d.division != 0.
相反,我将我的解决方案称为“不要吝啬关系”:
Department
----------
DepartmentID (PK) int NOT NULL
DepartmentName varchar(50) NOT NULL
Division
--------
DivisionID (PK) int NOT NULL
DepartmentID (FK) int NOT NULL
DivisonName varchar(50) NOT NULL
Article
-------
ArticleID (PK) int NOT NULL
ArticleName varchar(50) NOT NULL
ArticleBelongsToDepartment
--------------------------
ArticleID (PK) (FK) int NOT NULL
DepartmentID (FK) int NOT NULL
ArticleBelongsToDivision
--------------------------
ArticleID (PK) (FK) int NOT NULL
DivisionID (FK) int NOT NULL
现在,如何执行一些已经提出的约束?为了解决这个问题,您可以创建一个“文章箱”,其中每篇文章都必须属于一个箱,并且部门和部门都有箱。
但是,这已成为杂草,您将无法解决所有案例 - 一篇文章依赖于一个部门、一个部门或一个垃圾箱,或者它不是。要么部门依赖于 bin,要么 bin 依赖于部门。其中一些问题最好通过事务和存储过程来回答,也许是夜间完整性检查。
【讨论】:
【参考方案4】:部门和部门应存储在同一张表中。像这样:
DepDiv
----------
ID (PK) int NOT NULL
Name varchar(50) NOT NULL
Type int -- ex.: 1 for department, 2 for division, etc., incase you need to differentiate later
它们是如此相似的元素——你应该把它们一视同仁。
在那之后,不再需要复杂的逻辑 re: id 编号范围。无论如何,这种方法太不可扩展了。
祝你好运。
【讨论】:
【参考方案5】:re: 他使用虚构的规则定义了架构(因为没有更好的术语),所有 DepartmentID 都在 1 到 100 之间,所有 DivisionID 都在 101 到 200 之间。
如果这是他想要做的,他应该使用另一个字段,例如 isDepartment yes/no。然后,他将有一个包含 ID、名称和 isDepartment 的部门和部门表,并且 ID 字段将是 Article 表中的 FK。
这将解决重叠的部门和部门 ID,但不能解决部门和部门之间的一对多关系。要强制执行这种关系,您需要两个表。
您还可以在与 Article 具有 FK 关系的部门表和部门表中引入 AuthorID 字段。那可能是一个自动生成的字段。这是一种对除法表中的复合键进行规范化的方法。
【讨论】:
以上是关于在单个列中使用来自多个表的 ID的主要内容,如果未能解决你的问题,请参考以下文章