为什么你不能有一个多态关联的外键?

为什么你不能有一个多态关联的外键,比如下面的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中声明该约束。

多态关联devise破坏了关系数据库devise的规则。 我不build议使用它。

有几个select:

  • 独占弧:创build多个外键列,每个列引用一个父对象。 强制确保其中一个外键可以是非空的。

  • 扭转关系:使用三个多对多表,每个引用评论和一个相应的父母。

  • 具体Supertable:而不是隐式的“可评论的”超类,创build一个真正的表,每个父表引用。 然后把你的评论链接到那个超可靠的。 伪钢轨代码将是类似于以下内容(我不是一个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反模式:避免数据库编程的陷阱” 。


回复您的评论:是的,我知道还有另一列logging了外键所指的表名。 SQL中的外键不支持此devise。

例如,如果插入注释并将“Video”命名为该Comment的父表的名称,会发生什么情况? 没有名为“video”的表格存在。 插入是否应该中止一个错误? 什么约束被侵犯? RDBMS如何知道这个列是应该命名一个现有的表? 它如何处理不区分大小写的表名?

同样,如果删除“ Events表,但在“ Comments中有行表示“事件”作为其父项,那么结果应该是什么? 应该放弃drop table吗? Comments中的行应该是孤儿吗? 他们是否应该改为参考其他现有的表格,如Articles ? 当指向“ Articles时,用于指向“ Events ”的id值是否有意义?

这些困境都是由于多态关联依赖于使用数据(即string值)来引用元数据(表名)的事实。 这不被SQL支持。 数据和元数据是分开的。


我很难把你的“具体Supertable”提案包裹起来。

  • Commentable定义为一个真正的SQL表,而不仅仅是Rails模型定义中的一个形容词。 没有其他列是必要的。

     CREATE TABLE Commentable ( id INT AUTO_INCREMENT PRIMARY KEY ) TYPE=InnoDB; 
  • 定义表ArticlesPhotosEvents作为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; 
  • 当你想创build一个Article (例如),你也必须在Commentable创build一个新的行。 PhotosEvents也是如此。

     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(), ... ); 
  • 当您想要创build一个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; 
  • 当你只有一个评论的编号,你想find什么可评论的资源这是一个评论。 为此,您可能会发现可注释表指定它所引用的资源是有帮助的。

     SELECT commentable_id, commentable_type FROM Commentable t JOIN Comments c ON (t.id = c.commentable_id) WHERE c.id = 42; 

    然后,您需要运行第二个查询来从相应的资源表(照片,文章等)中获取数据,然后从commentable_type发现要join的表。 你不能在同一个查询中执行它,因为SQL要求显式地命名表; 您无法join由相同查询中的数据结果确定的表格。

无可否认,其中一些步骤违反了Rails使用的约定。 但是Rails约定在正确的关系数据库devise方面是错误的。

Bill Karwin是正确的,外键不能用于多态关系,因为SQL没有真正的本原概念多态关系。 但是如果你有一个外键的目标是强制引用完整性,你可以通过触发器来模拟它。 这得到数据库的具体情况,但下面是我创build的一些最近的触发器来模拟多态关系上外键的级联删除行为:

 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表中的logging或agents表中的logging可以与subscribers表中的logging相关联。