为什么在多态关联中没有外键,例如下面表示为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
答案 0 :(得分:161)
外键必须仅引用一个父表。这是SQL语法和关系理论的基础。
多态关联是指给定列可以引用两个或更多个父表中的任何一个。你无法在SQL中声明这个约束。
多态关联设计打破了关系数据库设计的规则。我不建议使用它。
有几种选择:
独占弧:创建多个外键列,每列引用一个父键。强制确切地说,其中一个外键可以是非NULL。
撤销关系:使用三个多对多表,每个表引用注释和相应的父级。
Concrete Supertable:创建一个每个父表引用的实际表,而不是隐式的“可注释”超类。然后将您的评论链接到该超级表格。伪轨代码将类似于以下内容(我不是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中的外键不支持此设计。
例如,如果您插入注释并将“Video”命名为Comment
的父表名称,会发生什么?没有名为“Video”的表格。插入是否应该中止错误?违反了什么约束? RDBMS如何知道该列应该命名现有的表?它如何处理不区分大小写的表名?
同样,如果删除Events
表,但Comments
中的行指示事件为其父项,那么应该是什么结果?丢弃表应该中止吗? Comments
中的行应该是孤立的吗?他们是否应该更改为引用另一个现有表格,例如Articles
?在指向Events
时,用于指向Articles
的ID值是否有意义?
这些困境都归因于多态关联依赖于使用数据(即字符串值)来引用元数据(表名)。 SQL不支持此功能。数据和元数据是分开的。
我很难围绕你的“具体的Supertable”提案。
将Commentable
定义为真正的SQL表,而不仅仅是Rails模型定义中的形容词。不需要其他专栏。
CREATE TABLE Commentable (
id INT AUTO_INCREMENT PRIMARY KEY
) TYPE=InnoDB;
将表Articles
,Photos
和Events
定义为Commentable
的“子类”,方法是使其主键也是引用{的外键。 {1}}。
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
如果您只有评论的ID,并且想要找到哪个可评论的资源,那么它就是评论。为此,您可能会发现Commentable表有助于指定它引用的资源。
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;
然后,在从SELECT commentable_id, commentable_type FROM Commentable t
JOIN Comments c ON (t.id = c.commentable_id)
WHERE c.id = 42;
要加入的表中发现后,您需要运行第二个查询以从相应的资源表(照片,文章等)中获取数据。您无法在同一查询中执行此操作,因为SQL要求显式命名表;您无法加入由同一查询中的数据结果确定的表格。
不可否认,其中一些步骤违反了Rails使用的约定。但是Rails约定在适当的关系数据库设计方面是错误的。
答案 1 :(得分:0)
Bill 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
表中的记录有关。