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


81

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

3
只是为了清楚起见,OP并不是在讨论foreign_key可以传递给的选项belongs_to。OP正在谈论本机数据库的“外键约束”。那使我有些困惑。
约书亚·品特

Answers:


178

外键只能引用一个父表。这对于SQL语法和关系理论都是至关重要的。

多态关联是指给定的列可以引用两个或多个父表中的任何一个。您无法在SQL中声明该约束。

多态关联设计打破了关系数据库设计的规则。我不建议使用它。

有几种选择:

  • 专用弧: 创建多个外键列,每个外键列均引用一个父级。强制这些外键之一可以是非NULL。

  • 逆转关系: 使用三个多对多表,每个表都引用注释和各自的父级。

  • 具体 的父表:创建一个父表中每个表都引用的真实表,而不是隐式的“可注释”超类。然后将您的评论链接到该超级表。伪-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”作为该父表的名称,会发生什么情况Comment?没有名为“视频”的表。插入是否应该因错误而中止?违反了什么约束?RDBMS如何知道该列应该命名现有表?它如何处理不区分大小写的表名?

同样,如果删除Events表,但其中有行Comments表示“事件”为其父项,结果应该是什么?应该放弃放置表吗?是否应该Comments孤立行?他们应该更改为引用另一个现有表Articles吗?Events指向时指向的id值是否有意义Articles

这些困境都是由于多态关联取决于使用数据(即字符串值)来引用元数据(表名)这一事实。SQL不支持此功能。数据和元数据是分开的。


对于您的“混凝土超级表”提案,我很难解决。

  • 定义Commentable为真实的SQL表,而不仅仅是Rails模型定义中的形容词。无需其他列。

    CREATE TABLE Commentable (
      id INT AUTO_INCREMENT PRIMARY KEY
    ) TYPE=InnoDB;
    
  • 通过使其表的主键也成为外键引用来定义表ArticlesPhotos并将其Events作为表的“子类” 。CommentableCommentable

    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约定在适当的关系数据库设计方面是错误的。


2
感谢您的跟进。就像我们在同一页上一样,在Rails多态关联中,我们在Comment中使用两列作为外键。第一列包含目标行的ID,第二列告诉Active Record该键所在的模型(文章,照片或事件)。知道这一点,您是否还会推荐您提出的三种替代方案?我很难为您的“混凝土超级表”提案打个招呼。您说“将您的评论链接到该超级表”(可注释)是什么意思?
eggdrop

1
感谢您的解释。我想我理解您为什么说Rails约定在正确的关系数据库设计方面是错误的-该模式在某些方面类似于使用平面文件作为存储机制,因为它失去了执行各种关系约束的能力。
eggdrop

6
究竟。当“多态关联”文档本身说您不能使用外键约束时,应该是强烈的“代码味道”,它不是正确的关系数据库设计!
比尔·卡温

1
Concrete Supertable解决方案的缺点之一是它不对子表强制执行参照完整性。例如,事件行和照片行可能具有相同的commentable_id。当然,使用好的过程创建commentable_id并将其分配给子表应该避免这种情况,但是可能性仍然存在。
杰森·马滕斯

1
@ Mohamad,STI可以正常工作。如果您的父表使用STI,您仍然可以定义外键。甚至子表使用STI。
Bill Karwin

3

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


这很棒。关于如何创建类似触发器以确保新创建的多态关联指向有效类型和ID的任何想法?
cayblood
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.