为什么在多态关联中没有外键?

时间:2009-05-28 17:32:39

标签: ruby-on-rails database foreign-key-relationship polymorphic-associations

为什么在多态关联中没有外键,例如下面表示为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

2 个答案:

答案 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;
    
  • 将表ArticlesPhotosEvents定义为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中创建新行。 CommentablePhotos也是如此。

    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表中的记录有关。