84

为什么在多态关联中不能有外键,比如下面的 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
4

2 回答 2

185

外键只能引用一个父表。这是 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;
    
  • 将表ArticlesPhotos和定义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 约定是错误的。

于 2009-05-28T17:59:42.730 回答
4

Bill Karwin 是正确的,外键不能与多态关系一起使用,因为 SQL 没有真正的原生概念多态关系。但是,如果您拥有外键的目标是强制引用完整性,您可以通过触发器来模拟它。这得到了特定的数据库,但下面是我最近创建的一些触发器,用于模拟多态关系上外键的级联删除行为:

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

于 2016-11-08T15:22:36.653 回答