ポリモーフィックな関連付けでは外部キーを使用できないのはなぜですか?
質問
以下に 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 でその制約を宣言する方法はありません。
ポリモーフィック アソシエーションの設計は、リレーショナル データベース設計のルールを打ち破ります。使用はお勧めしません。
いくつかの代替案があります。
限定アーク: それぞれが 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
?「Video」という名前のテーブルは存在しません。挿入はエラーで中止されるべきですか?どのような制約が違反されていますか?RDBMS は、この列が既存のテーブルに名前を付けることになっているということをどのようにして知るのでしょうか?大文字と小文字を区別しないテーブル名はどのように処理されますか?
同様に、 Events
テーブルですが、行があります Comments
イベントが親であることを示している場合、結果はどうなるでしょうか?ドロップテーブルを中止する必要がありますか?行を入れる必要があります Comments
孤児になる?次のような別の既存のテーブルを参照するように変更する必要がありますか? Articles
?を指していた ID 値を実行します Events
~を指すときは意味がある Articles
?
これらのジレンマはすべて、ポリモーフィック アソシエーションがデータの使用に依存しているという事実によるものです (つまり、文字列値) を参照してメタデータ (テーブル名) を参照します。これは SQL ではサポートされていません。データとメタデータは別のものです。
あなたの「コンクリートスーパーテーブル」という提案を理解するのに苦労しています。
定義する
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
どのテーブルに参加するか。SQL ではテーブルに明示的に名前を付ける必要があるため、同じクエリ内でこれを行うことはできません。同じクエリ内のデータ結果によって決定されるテーブルに結合することはできません。
確かに、これらの手順の一部は、Rails で使用されている規則を破っています。しかし、適切なリレーショナル データベース設計という点では、Rails の規則は間違っています。
他のヒント
ビル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
テーブル内のレコードに関連付けることができます。