为什么在多态关联中不能有外键,例如下面作为 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 中声明该约束。

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

有几种选择:

  • 独家弧线: 创建多个外键列,每个外键列引用一个父级。强制要求这些外键之一可以为非 NULL。

  • 反转关系: 使用三个多对多表,每个表引用 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
    

我还在演讲中介绍了多态关联 SQL 中实用的面向对象模型, ,和我的书 SQL 反模式:避免数据库编程的陷阱.


回复您的评论:是的,我确实知道还有另一列记录了外键应该指向的表的名称。SQL 中的外键不支持这种设计。

例如,如果您插入一条评论并将其命名为“视频”作为其父表的名称,会发生什么情况 Comment?不存在名为“Video”的表。插入是否应该因错误而中止?违反了什么约束?RDBMS 如何知道该列应该命名现有表?它如何处理不区分大小写的表名?

同样,如果您删除 Events 表,但你有行 Comments 表明事件作为其父级,结果应该是什么?是否应该中止删除表?应该行 Comments 成为孤儿?他们是否应该更改为引用另一个现有表,例如 Articles?执行曾经指向的 id 值 Events 指向时有意义 Articles?

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


我很难理解你的“混凝土超级桌”提案。

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

    CREATE TABLE Commentable (
      id INT AUTO_INCREMENT PRIMARY KEY
    ) TYPE=InnoDB;
    
  • 定义表 Articles, Photos, , 和 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 也。也是如此 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(), ... );
    
  • 当你想创建一个 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 并且想要查找该评论针对的可评论资源时。为此,您可能会发现可注释表指定它引用的资源很有帮助。

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

其他提示

比尔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表中的记录。

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top