为什么多态关联中不能有外键?
题
为什么在多态关联中不能有外键,例如下面作为 Rails 模型表示的关联?
class Comment < ActiveRecord::Base
belongs_to :commentable, :polymorphic => true
end
class Article < ActiveRecord::Base
has_many :comments, :as => :commentable
end
class Photo < ActiveRecord::Base
has_many :comments, :as => :commentable
#...
end
class Event < ActiveRecord::Base
has_many :comments, :as => :commentable
end
解决方案
外键必须仅引用一个父表。这是 SQL 语法和关系理论的基础。
多态关联是指给定列可以引用两个或多个父表中的一个。您无法在 SQL 中声明该约束。
多态关联设计打破了关系数据库设计的规则。我不建议使用它。
有几种选择:
独家弧线: 创建多个外键列,每个外键列引用一个父级。强制要求这些外键之一可以为非 NULL。
反转关系: 使用三个多对多表,每个表引用 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
我还在演讲中介绍了多态关联 SQL 中实用的面向对象模型, ,和我的书 SQL 反模式:避免数据库编程的陷阱.
回复您的评论:是的,我确实知道还有另一列记录了外键应该指向的表的名称。SQL 中的外键不支持这种设计。
例如,如果您插入一条评论并将其命名为“视频”作为其父表的名称,会发生什么情况 Comment
?不存在名为“Video”的表。插入是否应该因错误而中止?违反了什么约束?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 并且想要查找该评论针对的可评论资源时。为此,您可能会发现可注释表指定它引用的资源很有帮助。
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 约定在正确的关系数据库设计方面是错误的。
其他提示
比尔Karwin是正确的外键不能与多态性关系来使用,因为SQL不是真的具有天然概念的多态关系。但是,如果你有外键的目标是实施参照完整性您可以通过触发模拟。这得到DB特定但低于也是近期一些触发器,我创建模拟级联删除多态性关系的外键的行为:
CREATE FUNCTION delete_related_brokerage_subscribers() RETURNS trigger AS $$
BEGIN
DELETE FROM subscribers
WHERE referrer_type = 'Brokerage' AND referrer_id = OLD.id;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER cascade_brokerage_subscriber_delete
AFTER DELETE ON brokerages
FOR EACH ROW EXECUTE PROCEDURE delete_related_brokerage_subscribers();
CREATE FUNCTION delete_related_agent_subscribers() RETURNS trigger AS $$
BEGIN
DELETE FROM subscribers
WHERE referrer_type = 'Agent' AND referrer_id = OLD.id;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER cascade_agent_subscriber_delete
AFTER DELETE ON agents
FOR EACH ROW EXECUTE PROCEDURE delete_related_agent_subscribers();
在我的代码在brokerages
表或agents
表中的记录的记录可以涉及在subscribers
表中的记录。