web-dev-qa-db-ja.com

なぜ多態的な関連付けに外部キーを持たないことができますか?

以下に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
73
eggdrop

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

多態的な関連付けとは、特定の列が2つ以上の親テーブルのいずれかを参照する場合です。 SQLでその制約を宣言する方法はありません。

Polymorphic Associationsデザインは、リレーショナルデータベースデザインのルールを破ります。使用はお勧めしません。

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

  • 排他的アーク:複数の外部キー列を作成し、それぞれが1つの親を参照します。これらの外部キーのいずれか1つだけがNULLにならないようにします。

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

  • 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
    

また、プレゼンテーションでのポリモーフィックな関連付け SQLの実用的なオブジェクト指向モデル 、および私の本 SQLアンチパターン:データベースプログラミングの落とし穴の回避 も取り上げます。


コメント:はい、外部キーが指していると思われるテーブルの名前を示す別の列があることを知っています。この設計は、SQLの外部キーではサポートされていません。

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

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

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


あなたの「Concrete Supertable」提案に頭を包むのに苦労しています。

  • Commentableを、Railsモデル定義の形容詞ではなく、実際のSQLテーブルとして定義します。他の列は不要です。

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

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

165
Bill Karwin

Bill 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テーブルのレコードに関連付けることができます。

0
Eric Anderson