外键只能引用一个父表。这是 SQL 语法和关系理论的基础。
多态关联是指给定列可以引用两个或多个父表中的任何一个。您无法在 SQL 中声明该约束。
多态关联设计打破了关系数据库设计的规则。我不建议使用它。
有几种选择:
独占弧: 创建多个外键列,每个都引用一个父项。强制这些外键中的一个可以是非空的。
反转关系: 使用三个多对多表,每个表引用 Comments 和各自的父级。
具体 超表:创建每个父表引用的真实表,而不是隐含的“可注释”超类。然后将您的评论链接到该超级表。伪 Rails 代码类似于以下内容(我不是 Rails 用户,因此将其视为指南,而不是文字代码):
class Commentable < ActiveRecord::Base
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :commentable
end
class Article < ActiveRecord::Base
belongs_to :commentable
end
class Photo < ActiveRecord::Base
belongs_to :commentable
end
class Event < ActiveRecord::Base
belongs_to :commentable
end
我还在我的演讲Practical Object-Oriented Models in SQL和我的书SQL Antipatterns: Avoiding the Pitfalls of Database Programming中介绍了多态关联。
回复您的评论:是的,我知道还有另一列记录了外键应该指向的表的名称。SQL 中的外键不支持这种设计。
例如,如果您插入评论并将“视频”命名为父表的名称,会发生Comment
什么?不存在名为“视频”的表。插入是否应该因错误而中止?违反了什么约束?RDBMS 如何知道该列应该命名现有表?它如何处理不区分大小写的表名?
同样,如果您删除Events
表,但其中的行将Comments
事件指示为其父级,那么结果应该是什么?丢弃表应该被中止吗?应该Comments
孤立行吗?他们是否应该更改以引用另一个现有表,例如Articles
?在指向 时,曾经指向的 id 值是否Events
有意义Articles
?
这些困境都是因为多态关联依赖于使用数据(即字符串值)来引用元数据(表名)。SQL 不支持此功能。数据和元数据是分开的。
我很难理解你的“混凝土超级表”提案。
定义Commentable
为真正的 SQL 表,而不仅仅是 Rails 模型定义中的形容词。不需要其他列。
CREATE TABLE Commentable (
id INT AUTO_INCREMENT PRIMARY KEY
) TYPE=InnoDB;
将表Articles
、Photos
和定义Events
为 的“子类” Commentable
,方法是使它们的主键也是引用的外键Commentable
。
CREATE TABLE Articles (
id INT PRIMARY KEY, -- not auto-increment
FOREIGN KEY (id) REFERENCES Commentable(id)
) TYPE=InnoDB;
-- similar for Photos and Events.
Comments
用外键定义表Commentable
。
CREATE TABLE Comments (
id INT PRIMARY KEY AUTO_INCREMENT,
commentable_id INT NOT NULL,
FOREIGN KEY (commentable_id) REFERENCES Commentable(id)
) TYPE=InnoDB;
当您想创建一个Article
(例如)时,您也必须在其中创建一个新行Commentable
。对于Photos
和也是如此Events
。
INSERT INTO Commentable (id) VALUES (DEFAULT); -- generate a new id 1
INSERT INTO Articles (id, ...) VALUES ( LAST_INSERT_ID(), ... );
INSERT INTO Commentable (id) VALUES (DEFAULT); -- generate a new id 2
INSERT INTO Photos (id, ...) VALUES ( LAST_INSERT_ID(), ... );
INSERT INTO Commentable (id) VALUES (DEFAULT); -- generate a new id 3
INSERT INTO Events (id, ...) VALUES ( LAST_INSERT_ID(), ... );
当您要创建 时Comment
,请使用 中存在的值Commentable
。
INSERT INTO Comments (id, commentable_id, ...)
VALUES (DEFAULT, 2, ...);
当你想查询给定的评论时Photo
,做一些连接:
SELECT * FROM Photos p JOIN Commentable t ON (p.id = t.id)
LEFT OUTER JOIN Comments c ON (t.id = c.commentable_id)
WHERE p.id = 2;
当您只有评论的 id 并且您想找到它是评论的可评论资源时。为此,您可能会发现 Commentable 表指定它引用的资源很有帮助。
SELECT commentable_id, commentable_type FROM Commentable t
JOIN Comments c ON (t.id = c.commentable_id)
WHERE c.id = 42;
commentable_type
然后,在发现要加入哪个表之后,您需要运行第二个查询以从相应的资源表(照片、文章等)中获取数据。您不能在同一个查询中执行此操作,因为 SQL 要求显式命名表;您无法加入由同一查询中的数据结果确定的表。
诚然,其中一些步骤打破了 Rails 使用的约定。但是在正确的关系数据库设计方面,Rails 约定是错误的。