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

egg*_*rop 75 database ruby-on-rails 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
Run Code Online (Sandbox Code Playgroud)

Bil*_*win 167

外键必须仅引用一个父表.这是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
    
    Run Code Online (Sandbox Code Playgroud)

我还在我的演示文稿中介绍了SQL中的实用面向对象模型,以及我的SQL Antipatterns:避免数据库编程的陷阱.


重新评论:是的,我知道还有另一列记录了外键所指的表的名称.SQL中的外键不支持此设计.

例如,如果您插入注释并将"Video"命名为父表的名称,会发生Comment什么?没有名为"Video"的表格.插入是否应该中止错误?违反了什么约束?RDBMS如何知道该列应该命名现有的表?它如何处理不区分大小写的表名?

同样,如果你删除Events表,但是你有行Comments表明事件是他们的父,那么结果应该是什么?丢弃表应该中止吗?行Comments应该成为孤儿吗?他们应该改变以引用另一个现有的表格,例如ArticlesEvents当指向时,过去指向的id值是否有意义Articles

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


我很难绕过你的"具体的Supertable"提案.

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

    CREATE TABLE Commentable (
      id INT AUTO_INCREMENT PRIMARY KEY
    ) TYPE=InnoDB;
    
    Run Code Online (Sandbox Code Playgroud)
  • 通过使其主键也是外键引用来定义表Articles,Photos以及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.
    
    Run Code Online (Sandbox Code Playgroud)
  • 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;
    
    Run Code Online (Sandbox Code Playgroud)
  • 如果要创建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(), ... );
    
    Run Code Online (Sandbox Code Playgroud)
  • 如果要创建a Comment,请使用存在的值Commentable.

    INSERT INTO Comments (id, commentable_id, ...)
    VALUES (DEFAULT, 2, ...);
    
    Run Code Online (Sandbox Code Playgroud)
  • 如果要查询给定的注释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;
    
    Run Code Online (Sandbox Code Playgroud)
  • 如果您只有评论的ID,并且想要找到哪个可评论的资源,那么它就是评论.为此,您可能会发现Commentable表有助于指定它引用的资源.

    SELECT commentable_id, commentable_type FROM Commentable t
    JOIN Comments c ON (t.id = c.commentable_id)
    WHERE c.id = 42;
    
    Run Code Online (Sandbox Code Playgroud)

    然后,在发现要从commentable_type哪个表加入之后,您需要运行第二个查询以从相应的资源表(照片,文章等)中获取数据.您无法在同一查询中执行此操作,因为SQL要求显式命名表; 您无法加入由同一查询中的数据结果确定的表.

不可否认,其中一些步骤违反了Rails使用的约定.但是Rails约定在适当的关系数据库设计方面是错误的.

  • 究竟.当Polymorphic Associations文档本身表示你不能使用外键约束时,它应该是一个强大的"代码味道",它不是正确的关系数据库设计! (6认同)
  • 感谢您的跟进.就这样我们在同一页面上,在Rails中,多态关联在我们的Comment中使用了两列外键.一列保存目标行的id,第二列告诉Active Record该键所在的模型(文章,照片或事件).知道这一点,你还会推荐你提出的三个替代方案吗?我很难绕过你的"具体的Supertable"提案.当你说"将你的评论链接到那个超级表格"(评论)时,你是什么意思? (2认同)