Чому ви не можете мати зовнішній ключ у поліморфній асоціації?


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
Просто для ясності інших, УП не говорить про foreign_keyваріант, на який можна перейти belongs_to. ОП говорить про "обмеження зовнішнього ключа" власної бази даних. Це мене на деякий час збентежило.
Джошуа Пінтер

Відповіді:


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?

Усі ці дилеми пов’язані з тим, що Polymorphic Associations залежить від використання даних (тобто значення рядка) для посилання на метадані (назва таблиці). Це не підтримується 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(), ... );
    
  • Коли ви хочете створити a 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
Дякуємо за подальші дії. Щоб ми були на одній сторінці, у поліморфних асоціаціях Rails використовуємо два стовпці у нашому Коментарі для зовнішнього ключа. Один стовпець містить ідентифікатор цільового рядка, а другий стовпець повідомляє Active Record, в якій моделі знаходиться цей ключ (стаття, фото чи подія). Знаючи це, ви все-таки порекомендуєте три запропоновані вами альтернативи? Я важко обмотую голову вашою пропозицією "Конкретна супертаблиця". Що ви маєте на увазі, коли говорите "прив’яжіть свої коментарі до цієї надзвичайної таблиці" (коментар)
яєчна крапля

1
Дякую за пояснення. Думаю, я розумію, чому ви вважаєте, що конвенції Rails є неправильними щодо належного дизайну реляційних баз даних - шаблон певним чином нагадує використання плоских файлів як механізму зберігання, оскільки втрачає здатність застосовувати різні реляційні обмеження.
баклажан

7
Точно так. Має бути сильним «запахом коду», що це не правильний дизайн реляційної бази даних, коли сама документація Polymorphic Associations говорить, що ви не можете використовувати обмеження зовнішнього ключа!
Білл Карвін,

1
Одним з недоліків рішення Concrete Supertable є те, що воно не забезпечує довідкову цілісність у дочірній таблиці. Наприклад, для рядків "Події" та "Фотографії" можна мати однаковий ідентифікатор commentable_id. Звичайно, використовуючи хорошу процедуру для створення commentable_id та присвоєння його дочірній таблиці, слід уникати цієї ситуації, але така можливість все ще існує.
Джейсон Мартенс

1
@ Мохамад, ІПСШ працювала б нормально. Ви все ще можете визначити зовнішні ключі, якщо у вашій батьківській таблиці використовується STI. Або навіть якщо дочірній стіл використовував ІПСШ.
Білл Карвін,

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.