Почему у вас не может быть внешнего ключа в полиморфной ассоциации?

StackOverflow https://stackoverflow.com/questions/922184

Вопрос

Почему у вас не может быть внешнего ключа в полиморфной ассоциации, такой как та, которая представлена ниже в виде модели 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 таблица.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top