Почему вы не можете иметь внешний ключ в полиморфной ассоциации? - PullRequest
71 голосов
/ 28 мая 2009

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

Ответы [ 2 ]

161 голосов
/ 28 мая 2009

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

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

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

0 голосов
/ 08 ноября 2016

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

...