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

81

Почему у вас не может быть внешнего ключа в полиморфной ассоциации, такой как та, которая представлена ​​ниже как модель 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
яичная капля
источник
3
Просто для ясности других, OP не говорит о foreign_keyпараметре, который может быть передан belongs_to. OP говорит об «ограничении внешнего ключа» собственной базы данных. Это меня на время смутило.
Джошуа Пинтер

Ответы:

178

Внешний ключ должен ссылаться только на одну родительскую таблицу. Это фундаментально как для синтаксиса SQL, так и для теории отношений.

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

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

Есть несколько альтернатив:

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

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

  • Конкретная супертаблица: вместо неявного суперкласса с комментариями создайте реальную таблицу, на которую ссылается каждая из ваших родительских таблиц. Затем свяжите свои комментарии с этой надтаблицей. Код псевдорельсов будет примерно таким (я не являюсь пользователем 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? Имеют ли значения id, которые раньше указывали, 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 неверны в отношении правильного проектирования реляционной базы данных.

Билл Карвин
источник
2
Спасибо за ответ. Так что мы находимся на одной странице, в полиморфных ассоциациях Rails для внешнего ключа используются два столбца в нашем комментарии. Один столбец содержит идентификатор целевой строки, а второй столбец сообщает Active Record, в какой модели находится этот ключ (статья, фотография или событие). Зная это, порекомендовали бы вы по-прежнему три предложенных вами альтернативы? Мне трудно осмыслить ваше предложение "Конкретная надстройка". Что вы имеете в виду, когда говорите «свяжите свои комментарии с этой надтаблицей» (комментарий)?
eggdrop
1
Спасибо за объяснение. Думаю, я понимаю, почему вы говорите, что соглашения Rails ошибочны в отношении правильного проектирования реляционной базы данных - шаблон в некотором роде напоминает использование плоских файлов в качестве механизма хранения, поскольку он теряет способность обеспечивать соблюдение различных реляционных ограничений.
eggdrop
7
Точно. Когда в самой документации полиморфных ассоциаций говорится, что вы не можете использовать ограничения внешнего ключа, это должно быть сильным «запахом кода», что это неправильный дизайн реляционной базы данных!
Билл Карвин
1
Одним из недостатков решения Concrete Supertable является то, что оно не обеспечивает ссылочную целостность дочерней таблицы. Например, строка «События» и строка «Фотографии» могут иметь одинаковый commentable_id. Конечно, использование хорошей процедуры для создания commentable_id и присвоения его дочерней таблице должно избежать этой ситуации, но такая возможность все еще существует.
Джейсон Мартенс
1
@ Mohamad, STI подойдет. Вы все равно можете определять внешние ключи, если ваша родительская таблица использует STI. Или даже если в дочерней таблице используется STI.
Bill Karwin
3

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

Эрик Андерсон
источник
Это замечательно. Есть идеи, как можно создать аналогичный триггер, чтобы убедиться, что вновь созданные полиморфные ассоциации указывают на допустимый тип и идентификатор?
cayblood