Почему у вас не может быть внешнего ключа в полиморфной ассоциации?
Вопрос
Почему у вас не может быть внешнего ключа в полиморфной ассоциации, такой как та, которая представлена ниже в виде модели 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
Решение
Внешний ключ должен ссылаться только на одну родительскую таблицу.Это фундаментально как для синтаксиса SQL, так и для теории отношений.
Полиморфная ассоциация - это когда данный столбец может ссылаться на любую из двух или более родительских таблиц.Вы никак не можете объявить это ограничение в SQL.
Дизайн полиморфных ассоциаций нарушает правила проектирования реляционных баз данных.Я не рекомендую им пользоваться.
Есть несколько альтернатив:
Эксклюзивные Дуги: Создайте несколько столбцов внешнего ключа, каждый из которых ссылается на одного родительского пользователя.Убедитесь, что только один из этих внешних ключей может быть ненулевым.
Измените Соотношение Вспять: Используйте три таблицы "многие ко многим", каждая из которых ссылается на комментарии и соответствующий родительский элемент.
Бетонный Супертяжелый стол: Вместо неявного "комментируемого" суперкласса создайте реальную таблицу, на которую ссылается каждая из ваших родительских таблиц.Затем привяжите свои комментарии к этой супертаблице.Код псевдо-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" не существует.Должна ли вставка быть прервана с ошибкой?Какое ограничение нарушается?Как СУБД узнает, что этот столбец должен давать имя существующей таблице?Как он обрабатывает имена таблиц без учета регистра?
Аналогично, если вы отбросите Events
таблица, но у вас есть строки в Comments
которые указывают События в качестве их родительских, каким должен быть результат?Следует ли прервать удаление таблицы?Должны ли строки в Comments
стать сиротой?Должны ли они измениться, чтобы ссылаться на другую существующую таблицу, например Articles
?Укажите значения идентификатора, которые раньше указывали на Events
имеет ли какой-либо смысл указывать на Articles
?
Все эти дилеммы связаны с тем фактом, что полиморфные ассоциации зависят от использования данных (т. е.строковое значение) для ссылки на метаданные (имя таблицы).Это не поддерживается SQL.Данные и метаданные разделены.
Мне трудно осмыслить ваше предложение о "Бетонном супертексте".
Определить
Commentable
как настоящая таблица SQL, а не просто прилагательное в определении вашей модели Rails.Никакие другие столбцы не нужны.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;
Когда у вас есть только идентификатор комментария, и вы хотите найти, для какого комментируемого ресурса это комментарий.Для этого вы можете обнаружить, что в таблице с комментариями полезно указать, на какой ресурс она ссылается.
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 неверны с точки зрения правильного проектирования реляционной базы данных.
Другие советы
Билл Карвин прав в том, что внешние ключи не могут использоваться с полиморфными отношениями из-за того, что в 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
таблица.