ポリモーフィックアソシエーションで外部キーを使用できないのはなぜですか?


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は、ネイティブデータベースの「外部キー制約」について話します。それはしばらく私を混乱させました。
Joshua Pinter 2018年

回答:


178

外部キーは、1つの親テーブルのみを参照する必要があります。これは、SQL構文とリレーショナル理論の両方の基本です。

ポリモーフィックアソシエーションは、特定の列が2つ以上の親テーブルのいずれかを参照する場合です。SQLでその制約を宣言する方法はありません。

ポリモーフィックアソシエーションの設計は、リレーショナルデータベースの設計のルールに違反しています。使用はお勧めしません。

いくつかの選択肢があります:

  • 排他的アーク: それぞれが1つの親を参照する複数の外部キー列を作成します。これらの外部キーの1つだけがNULL以外になる可能性があることを強制します。

  • 関係を逆にする: 3つの多対多のテーブルを使用し、それぞれがコメントとそれぞれの親を参照します。

  • 具体的なスーパーテーブル: 暗黙の「コメント可能な」スーパークラスの代わりに、各親テーブルが参照する実際のテーブルを作成します。次に、コメントをそのスーパーテーブルにリンクします。疑似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の外部キーではサポートされていません。

たとえば、コメントを挿入し、その親テーブルの名前として「Video」という名前を付けた場合はどうなりますCommentか?「ビデオ」という名前のテーブルは存在しません。挿入をエラーで中止する必要がありますか?どのような制約に違反していますか?RDBMSは、この列が既存のテーブルに名前を付けることになっていることをどのように認識しますか?大文字と小文字を区別しないテーブル名はどのように処理されますか?

同様に、Eventsテーブルを削除しても、Commentsイベントを親として示す行がある場合、結果はどうなるでしょうか。ドロップテーブルを中止する必要がありますか?行をComments孤立させる必要がありますか?次のような別の既存のテーブルを参照するように変更する必要がありArticlesますか?をEvents指すときに意味をなすために、以前は指していたid値を実行しますArticlesますか?

これらのジレンマはすべて、ポリモーフィックアソシエーションがメタデータ(テーブル名)を参照するためにデータ(つまり文字列値)を使用することに依存しているという事実によるものです。これはSQLではサポートされていません。データとメタデータは分離されています。


あなたの「ConcreteSupertable」の提案に頭を悩ませるのに苦労しています。

  • CommentableRailsモデル定義の形容詞だけでなく、実際のSQLテーブルとして定義します。他の列は必要ありません。

    CREATE TABLE Commentable (
      id INT AUTO_INCREMENT PRIMARY KEY
    ) TYPE=InnoDB;
    
  • テーブルを定義しArticlesPhotos、および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すぎ。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どのテーブルから結合するかを検出した後、2番目のクエリを実行してそれぞれのリソーステーブル(写真、記事など)からデータを取得する必要があります。SQLではテーブルに明示的に名前を付ける必要があるため、同じクエリでこれを行うことはできません。同じクエリのデータ結果によって決定されるテーブルに結合することはできません。

確かに、これらの手順のいくつかは、Railsで使用されている規則に違反しています。しかし、Railsの規則は、適切なリレーショナルデータベースの設計に関して間違っています。


2
フォローアップしていただきありがとうございます。同じページにいるので、Railsでは、ポリモーフィックアソシエーションは外部キーのコメントで2つの列を使用します。1つの列はターゲット行のIDを保持し、2番目の列はそのキーがどのモデル(記事、写真、またはイベント)にあるかをActiveRecordに通知します。これを知って、あなたはまだあなたが提案した3つの選択肢をお勧めしますか?あなたの「ConcreteSupertable」の提案に頭を悩ませるのに苦労しています。「コメントをそのスーパーテーブルにリンクする」(コメント可能)とはどういう意味ですか?
eggdrop

1
説明ありがとうございます。Railsの規則が適切なリレーショナルデータベースの設計に関して間違っていると言う理由を理解していると思います。このパターンは、さまざまなリレーショナル制約を適用する機能が失われるという点で、ストレージメカニズムとしてフラットファイルを使用することに似ています。
eggdrop

7
丁度。ポリモーフィックアソシエーションのドキュメント自体に外部キー制約を使用できないと記載されている場合、リレーショナルデータベースの設計が正しくないことは、強い「コードの臭い」であるはずです。
ビルカーウィン

1
Concrete Supertableソリューションの欠点の1つは、子テーブルに参照整合性を適用しないことです。たとえば、Events行とPhotos行が同じcommentable_idを持つ可能性があります。確かに、適切な手順を使用してcommentable_idを作成し、それを子テーブルに割り当てると、この状況を回避できますが、可能性はまだ存在します。
Jason Martens 2013年

1
@モハマド、STIはうまくいくでしょう。親テーブルがSTIを使用している場合でも、外部キーを定義できます。または、子テーブルがSTIを使用した場合でも。
ビルカーウィン2013

3

ビル・カーウィンは、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ます。


これは素晴らしい。新しく作成されたポリモーフィックな関連付けが有効なタイプとIDを指していることを確認するために、同様のトリガーを作成する方法についてのアイデアはありますか?
cayblood
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.