为什么多态关联中不能有外键?

88

为什么不能在多态关联中使用外键,例如以下表示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
为了让其他人更加清晰地理解,原帖并不是在谈论可以传递给belongs_toforeign_key 选项。原帖在谈论本地数据库的“外键约束”。这让我困惑了一段时间。 - Joshua Pinter
3个回答

197

外键必须引用一个父表。 这对于SQL语法和关系理论来说是基本的。

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

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

有几种替代方案:

  • Exclusive Arcs: Create multiple foreign key columns, each referencing one parent. Enforce that exactly one of these foreign keys can be non-NULL.

  • Reverse the Relationship: Use three many-to-many tables, each references Comments and a respective parent.

  • Concrete Supertable: Instead of the implicit "commentable" superclass, create a real table that each of your parent tables references. Then link your Comments to that supertable. Pseudo-rails code would be something like the following (I'm not a Rails user, so treat this as a guideline, not literal code):

     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 Volume 1: Avoiding the Pitfalls of Database Programming中也涉及多态关联。


关于您的评论:是的,我知道还有另一列记录了外键所指向的表的名称。但是,这种设计不受SQL外键支持。

例如,如果您插入一个名为“ Video”的Comment,则该父表的名称为“ Video”。但是没有名为“ Video”的表存在。应该中止插入并显示错误吗?违反了哪些约束条件?RDBMS如何知道此列应命名现有表格?它如何处理大小写不敏感的表格名称?

同样,如果删除Events表,但是在Comments中有指示Events为其父级的行,结果应该是什么?应该中止删除表操作吗?Comments中的行是否应成为孤儿行?它们是否更改为引用其他现有表格(例如Articles)?指向Events的id值在指向Articles时是否有意义?

所有这些困境都源于Polymorphic Associations依赖使用数据(即字符串值)来引用元数据(表格名称)。这在SQL中不受支持。数据和元数据是分开的。


我很难理解您的“Concrete Supertable”建议。

  • Define Commentable as a real SQL table, not just an adjective in your Rails model definition. No other columns are necessary.

     CREATE TABLE Commentable (
       id INT AUTO_INCREMENT PRIMARY KEY
     ) TYPE=InnoDB;
    
  • Define the tables Articles, Photos, and Events as "subclasses" of Commentable, by making their primary key be also a foreign key referencing Commentable.

     CREATE TABLE Articles (
       id INT PRIMARY KEY, -- not auto-increment
       FOREIGN KEY (id) REFERENCES Commentable(id)
     ) TYPE=InnoDB;
    
     -- similar for Photos and Events.
    
  • Define the Comments table with a foreign key to Commentable.

     CREATE TABLE Comments (
       id INT PRIMARY KEY AUTO_INCREMENT,
       commentable_id INT NOT NULL,
       FOREIGN KEY (commentable_id) REFERENCES Commentable(id)
     ) TYPE=InnoDB;
    
  • When you want to create an Article (for instance), you must create a new row in Commentable too. So too for Photos and 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(), ... );
    
  • When you want to create a Comment, use a value that exists in Commentable.

     INSERT INTO Comments (id, commentable_id, ...)
     VALUES (DEFAULT, 2, ...);
    
  • When you want to query comments of a given Photo, do some joins:

     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;
    
  • When you have only the id of a comment and you want to find what commentable resource it's a comment for. For this, you may find that it's helpful for the Commentable table to designate which resource it references.

     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中的多态关联在我们的评论中使用两列外键。一列保存目标行的ID,另一列告诉Active Record该键位于哪个模型中(Article、Photo或Event)。知道这一点,您仍然会推荐您提出的三种替代方案吗?我很难理解您的“具体超级表”建议。当您说“将您的评论链接到该超级表”(Commentable)时,您是什么意思? - eggdrop
7
没错。当多态关联文档本身说你不能使用外键约束时,这应该是一种明显的“代码异味”,表明它不是正确的关系数据库设计! - Bill Karwin
1
具体的超级表解决方案的一个缺点是它不会在子表上强制执行引用完整性。例如,一个事件行和一个照片行可以有相同的commentable_id。当然,使用良好的程序来创建commentable_id并将其分配给子表应该避免这种情况,但可能性仍然存在。 - Jason Martens
1
@Mohamad,STI 可以很好地工作。如果您的父表使用 STI,则仍然可以定义外键。即使子表使用 STI 也是如此。 - Bill Karwin
1
@antinome,是的,如果您按照我展示的方式创建具有真实外键约束的表,那么除非在Commentable表中存在相应的id,否则您将无法在Articles表中创建行。 - Bill Karwin
显示剩余26条评论

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

1
这很棒。有什么想法可以创建类似的触发器,以确保新创建的多态关联指向有效的类型和ID吗? - cayblood

0

假设你想在 SQL 中创建一个点赞系统。在多态关联中,你需要创建一个 LIKE 表并包含以下列:

id_of_like    user_id     liked_id   liked_type
  1            12          3          image
  2            3           5          video

liked_id 指的是图像或视频的 ID。

通常在创建表时,您会设置一个 外键 并告诉 SQL 数据库这个外键引用哪个表。在插入行之前,您的数据库将检查当前外键值是否有效。例如,在一个外键引用 USER 表的表中,您的数据库将检查当前外键是否存在于 USER 表中。这种验证确保了您的数据库的一致性。

假设在上面的表中,我们以某种方式将 liked_id 设置为 FOREIGN KEY,那么数据库如何知道要访问哪个表(IMAGE 或 VIDEO)来验证当前键值?liked_type 不是给数据库管理器看的,而是给人类阅读的,数据库管理器不会读取它来检查要去哪个表。

想象一下,我插入了一个新行,其中 liked_id=333333

id_of_like    user_id     liked_id   liked_type
  1            12          333333     image

你的数据库管理器将无法验证 id=333333 是否存在于 IMAGE 表中。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接