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

发布于 2024-07-21 06:02:22 字数 429 浏览 12 评论 0原文

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

Why can you not have a foreign key in a polymorphic association, such as the one represented below as a Rails model?

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

白云悠悠 2024-07-28 06:02:22

外键必须仅引用一个父表。 这是 SQL 语法和关系理论的基础。

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

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

有几种替代方案:

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

  • 反转关系:使用三个多对多表,每个表都引用 Comments 和各自的父表。

  • 具体超级表:创建每个父表引用的真实表,而不是隐式的“可注释”超类。 然后将您的评论链接到该超级表。 伪 Rails 代码将类似于以下内容(我不是 Rails 用户,因此请将其视为指南,而不是文字代码):

     类可注释 <;   ActiveRecord::基础 
         有_很多:评论 
       结尾 
    
       类注释<   ActiveRecord::基础 
         属于:可评论 
       结尾 
    
       类文章<   ActiveRecord::基础 
         属于:可评论 
       结尾 
    
       班级照片<   ActiveRecord::基础 
         属于:可评论 
       结尾 
    
       类事件<   ActiveRecord::基础 
         属于:可评论 
       结尾 
      

我还在演示文稿中介绍了多态关联Slideshare.net/billkarwin/practical-object-driven-models-in-sql" rel="noreferrer">SQL 中的实用面向对象模型,以及我的书 SQL 反模式卷 1:避免数据库编程的陷阱


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

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

同样,如果您删除 Events 表,但 Comments 中有行指示 Events 作为其父项,结果应该是什么? 是否应该中止删除表? Comments 中的行应该被孤立吗? 它们是否应该更改为引用另一个现有表,例如 Articles? 用于指向 Events 的 id 值在指向 Articles 时是否有意义?

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


我很难理解你的“具体超级表”提案。

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

     CREATE TABLE 可注释 ( 
         id INT 自动递增主键 
       ) 类型=InnoDB; 
      
  • 将表 ArticlesPhotosEvents 定义为 Commentable 的“子类”,方法是使它们主键也可以是引用Commentable的外键。

     创建表文章 ( 
         id INT PRIMARY KEY, -- 不自增 
         外键 (id) 参考文献 可评论 (id) 
       ) 类型=InnoDB; 
    
       -- 照片和事件类似。 
      
  • 使用 Commentable 的外键定义 Comments 表。

     CREATE TABLE 注释 ( 
         id INT 主键自动递增, 
         commentable_id INT NOT NULL, 
         外键 (commentable_id) 参考文献 Commentable(id) 
       ) 类型=InnoDB; 
      
  • 例如,当您想要创建Article时,您也必须在Commentable中创建一个新行。 照片活动也是如此。

     INSERT INTO 可注释 (id) 值(默认);   -- 生成新的id 1 
       INSERT INTO 文章 (id, ...) VALUES ( LAST_INSERT_ID(), ... ); 
    
       插入可注释 (id) 值(默认);   -- 生成新的id 2 
       插入照片 (id, ...) 值 ( LAST_INSERT_ID(), ... ); 
    
       插入可注释 (id) 值(默认);   -- 生成新的id 3 
       INSERT INTO 事件 (id, ...) VALUES ( LAST_INSERT_ID(), ... ); 
      
  • 当您想要创建Comment时,请使用Commentable中存在的值。

     INSERT INTO Comments (id, commentable_id, ...) 
       值(默认值,2,...); 
      
  • 当您想要查询给定照片的评论时,请执行一些连接:

     从照片中选择 * p JOIN 可评论 t ON (p.id = t.id) 
       LEFT OUTER JOIN 注释 c ON (t.id = c.commentable_id) 
       其中 p.id = 2; 
      
  • 当您只有评论的 ID 并且您想要查找该评论针对的可评论资源时。 为此,您可能会发现可评论表指定它引用的资源很有帮助。

     SELECT commentable_id, commentable_type FROM Commentable t 
       JOIN 评论 c ON (t.id = c.commentable_id) 
       其中 c.id = 42; 
      

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

诚然,其中一些步骤违反了 Rails 使用的约定。 但是 Rails 约定在正确的关系数据库设计方面是错误的。

A foreign key must reference only one parent table. This is fundamental to both SQL syntax, and relational theory.

A Polymorphic Association is when a given column may reference either of two or more parent tables. There's no way you can declare that constraint in SQL.

The Polymorphic Associations design breaks rules of relational database design. I don't recommend using it.

There are several alternatives:

  • 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
    

I also cover polymorphic associations in my presentation Practical Object-Oriented Models in SQL, and my book SQL Antipatterns Volume 1: Avoiding the Pitfalls of Database Programming.


Re your comment: Yes, I do know that there's another column that notes the name of the table that the foreign key supposedly points to. This design is not supported by foreign keys in SQL.

What happens, for instance, if you insert a Comment and name "Video" as the name of the parent table for that Comment? No table named "Video" exists. Should the insert be aborted with an error? What constraint is being violated? How does the RDBMS know that this column is supposed to name an existing table? How does it handle case-insensitive table names?

Likewise, if you drop the Events table, but you have rows in Comments that indicate Events as their parent, what should be the result? Should the drop table be aborted? Should rows in Comments be orphaned? Should they change to refer to another existing table such as Articles? Do the id values that used to point to Events make any sense when pointing to Articles?

These dilemmas are all due to the fact that Polymorphic Associations depends on using data (i.e. a string value) to refer to metadata (a table name). This is not supported by SQL. Data and metadata are separate.


I'm having a hard time wrapping my head around your "Concrete Supertable" proposal.

  • 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;
    

Then you'd need to run a second query to get data from the respective resource table (Photos, Articles, etc.), after discovering from commentable_type which table to join to. You can't do it in the same query, because SQL requires that tables be named explicitly; you can't join to a table determined by data results in the same query.

Admittedly, some of these steps break the conventions used by Rails. But the Rails conventions are wrong with respect to proper relational database design.

小草泠泠 2024-07-28 06:02:22

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

Bill Karwin is correct that foreign keys cannot be used with polymorphic relationships due to SQL not really having a native concept polymorphic relationships. But if your goal of having a foreign key is to enforce referential integrity you can simulate it via triggers. This gets DB specific but below is some recent triggers I created to simulate the cascading delete behavior of a foreign key on a polymorphic relationship:

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();

In my code a record in the brokerages table or a record in the agents table can relate to a record in the subscribers table.

倦话 2024-07-28 06:02:22

假设您想用 SQL 创建一个类似的系统。 在多态关联中,您可以使用这些列创建一个 LIKE 表:

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

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

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

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

想象一下,我插入了一个带有 liked_id=333333 的新行,

id_of_like    user_id     liked_id   liked_type
  1            12          333333     image

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

Let's say you want to create a like system in SQL. in polymorphic association you create a LIKE table with those columns:

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

liked_id refers to id of image or id of video.

Normally when you create a table, you set a foreign key and tell the SQL database which table will this foreign key reference. Before you insert the row, your database will check if the current foreign key value is valid or not. for example if in a table where foreign key references USER table, your database will check if the current foreign key exists in the USER table. This verification makes sure your database is consistent.

Let's say in the above table, we somehow set the liked_id as a FOREIGN KEY, how the database would know which table (IMAGE or VIDEO) to visit to validate the current key value? liked_type is not for the database manager it is for humans to read it, database manager does not read it to check which table to go.

Imagine I inserted a new row with liked_id=333333

id_of_like    user_id     liked_id   liked_type
  1            12          333333     image

your database manager will not be able to validate if id=333333 exist in IMAGE table or not.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文