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

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

Полиморфная ассоциация - это когда данный столбец может ссылаться на одну или две родительские таблицы. Невозможно объявить это ограничение в SQL.

Конструкция полиморфных ассоциаций нарушает правила проектирования реляционных баз данных. Я не рекомендую использовать его.

Существует несколько альтернатив:

  • Эксклюзивные дуги: Создайте несколько столбцов внешнего ключа, каждый из которых ссылается на одного родителя. Убедитесь, что точно один из этих внешних ключей может быть не NULL.

  • Отменить отношение: Использовать три таблицы "многие-ко-многим", каждая ссылка "Комментарии" и соответствующий родитель.

  • 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 Antipatterns: устранение ошибок программирования баз данных.


Повторите свой комментарий: Да, я знаю, что есть еще один столбец, который отмечает имя таблицы, на которое якобы указывает внешний ключ. Этот дизайн не поддерживается внешними ключами в SQL.

Что происходит, например, если вы вставляете комментарий и имя "Видео" в качестве имени родительской таблицы для этого Comment? Нет таблицы с именем "Видео". Должна ли вставка быть прервана с ошибкой? Какое ограничение нарушается? Как RDBMS знает, что этот столбец должен назвать существующую таблицу? Как он обрабатывает имена таблиц без учета регистра?

Аналогично, если вы отбрасываете таблицу Events, но у вас есть строки в Comments, которые указывают на события как на их родителя, каков должен быть результат? Следует ли прервать таблицу отбрасывания? Должны ли строки в Comments осиротеть? Должны ли они измениться, чтобы ссылаться на другую существующую таблицу, такую ​​как Articles? Значения id, которые указывали на Events, имеют смысл при указании на Articles?

Эти дилеммы связаны с тем, что полиморфные ассоциации зависят от использования данных (т.е. строкового значения) для ссылки на метаданные (имя таблицы). Это не поддерживается SQL. Данные и метаданные являются отдельными.


Мне тяжело обволакивать ваше предложение "Concrete Supertable".

  • Определите 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;
    
  • Если у вас есть только идентификатор комментария, и вы хотите найти, для какого комментируемого ресурса это комментарий. Для этого вы можете обнаружить, что полезно, чтобы таблица Commentable указывала, какой ресурс он ссылается.

    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 ошибочны в отношении правильной структуры реляционной базы данных.

Ответ 2

Билл Карвин прав, что внешние ключи не могут использоваться с полиморфными отношениями из-за того, что 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.