以下に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
外部キーは、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;
テーブルArticles
、Photos
、および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のみがあり、コメント可能なリソースを見つけたい場合。このため、コメント可能なテーブルが参照するリソースを指定すると役立つ場合があります。
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の規則は、適切なリレーショナルデータベースの設計に関して間違っています。
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
テーブルのレコードに関連付けることができます。