Dlaczego nie możesz mieć klucza obcego w skojarzeniu polimorficznym?

81

Dlaczego nie możesz mieć klucza obcego w skojarzeniu polimorficznym, takim jak ten przedstawiony poniżej jako model 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
jajko
źródło
3
Dla jasności innych PO nie mówi o foreign_keyopcji, do której można przekazać belongs_to. OP mówi o „Ograniczeniu klucza obcego” rodzimej bazy danych. To mnie na chwilę zdezorientowało.
Joshua Pinter,

Odpowiedzi:

178

Klucz obcy musi odnosić się tylko do jednej tabeli nadrzędnej. Ma to fundamentalne znaczenie zarówno dla składni SQL, jak i teorii relacyjnej.

Skojarzenie polimorficzne występuje wtedy, gdy dana kolumna może odwoływać się do jednej z dwóch lub więcej tabel nadrzędnych. Nie ma możliwości zadeklarowania tego ograniczenia w SQL.

Projekt asocjacji polimorficznych łamie zasady projektowania relacyjnych baz danych. Nie polecam go używać.

Istnieje kilka alternatyw:

  • Ekskluzywne łuki: utwórz wiele kolumn z kluczami obcymi, z których każda odwołuje się do jednego rodzica. Wymuś, że dokładnie jeden z tych kluczy obcych może mieć wartość różną od NULL.

  • Odwróć relację: użyj trzech tabel wiele do wielu, z których każda odwołuje się do komentarzy i odpowiedniego elementu nadrzędnego.

  • Concrete Supertable: Zamiast niejawnej, „ możliwej do komentowania” nadklasy, utwórz rzeczywistą tabelę, do której odwołuje się każda z tabel nadrzędnych. Następnie połącz swoje komentarze z tym supertable. Kod pseudo-szyn wyglądałby mniej więcej tak (nie jestem użytkownikiem Railsów, więc potraktuj to jako wskazówkę, a nie kod dosłowny):

    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
    

Skojarzenia polimorficzne omawiam także w mojej prezentacji Praktyczne modele obiektowe w SQL oraz w mojej książce SQL Antipatterns: Avoiding the Pitfalls of Database Programming .


Odp. Twój komentarz: Tak, wiem, że istnieje inna kolumna, w której znajduje się nazwa tabeli, na którą rzekomo wskazuje klucz obcy. Ten projekt nie jest obsługiwany przez klucze obce w języku SQL.

Co się stanie, na przykład, jeśli wstawisz komentarz i nazwiesz „Wideo” jako nazwę tabeli nadrzędnej Comment? Nie istnieje tabela o nazwie „Wideo”. Czy wstawianie powinno zostać przerwane z powodu błędu? Jakie ograniczenie jest naruszane? Skąd RDBMS wie, że ta kolumna ma nazywać istniejącą tabelę? Jak obsługuje nazwy tabel bez rozróżniania wielkości liter?

Podobnie, jeśli usuniesz Eventstabelę, ale masz w niej wiersze Commentswskazujące zdarzenia jako ich element nadrzędny, jaki powinien być wynik? Czy opuszczanie stołu powinno zostać przerwane? Czy wiersze powinny Commentszostać osierocone? Czy powinny się zmienić, aby odwołać się do innej istniejącej tabeli, takiej jak Articles? Czy wartości id, które wskazywały, Eventsmają jakikolwiek sens, gdy wskazują na Articles?

Wszystkie te dylematy wynikają z faktu, że asocjacje polimorficzne polegają na używaniu danych (tj. Wartości ciągu) do odwoływania się do metadanych (nazwy tabeli). Nie jest to obsługiwane przez SQL. Dane i metadane są oddzielne.


Trudno mi się ogarnąć twoją propozycją „Concrete Supertable”.

  • Zdefiniuj Commentablejako prawdziwą tabelę SQL, a nie tylko przymiotnik w definicji modelu Rails. Żadne inne kolumny nie są potrzebne.

    CREATE TABLE Commentable (
      id INT AUTO_INCREMENT PRIMARY KEY
    ) TYPE=InnoDB;
    
  • Definiowanie tabel Articles, Photosi Eventsjako „podklasy” o Commentable, dokonując ich podstawowym kluczem być również kluczowym odsyłania zagranicznych Commentable.

    CREATE TABLE Articles (
      id INT PRIMARY KEY, -- not auto-increment
      FOREIGN KEY (id) REFERENCES Commentable(id)
    ) TYPE=InnoDB;
    
    -- similar for Photos and Events.
    
  • Zdefiniuj Commentstabelę za pomocą klucza obcego do Commentable.

    CREATE TABLE Comments (
      id INT PRIMARY KEY AUTO_INCREMENT,
      commentable_id INT NOT NULL,
      FOREIGN KEY (commentable_id) REFERENCES Commentable(id)
    ) TYPE=InnoDB;
    
  • Jeśli chcesz utworzyć Article(na przykład), musisz również utworzyć nowy wiersz w Commentable. To samo dotyczy Photosi 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(), ... );
    
  • Jeśli chcesz utworzyć Comment, użyj wartości, która istnieje w Commentable.

    INSERT INTO Comments (id, commentable_id, ...)
    VALUES (DEFAULT, 2, ...);
    
  • Jeśli chcesz odpytać o komentarze danego Photo, zrób kilka sprzężeń:

    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;
    
  • Gdy masz tylko identyfikator komentarza i chcesz znaleźć zasób, który można skomentować, jest to komentarz. W tym celu może się okazać, że tabela z możliwością komentowania powinna wskazać, do którego zasobu się odwołuje.

    SELECT commentable_id, commentable_type FROM Commentable t
    JOIN Comments c ON (t.id = c.commentable_id)
    WHERE c.id = 42;
    

    Następnie musisz uruchomić drugie zapytanie, aby pobrać dane z odpowiedniej tabeli zasobów (zdjęcia, artykuły itp.), Po znalezieniu commentable_typetabeli, do której chcesz dołączyć. Nie możesz tego zrobić w tym samym zapytaniu, ponieważ SQL wymaga, aby tabele były nazwane jawnie; nie możesz dołączyć do tabeli określonej przez wyniki danych w tym samym zapytaniu.

Trzeba przyznać, że niektóre z tych kroków łamią konwencje używane przez Railsy. Ale konwencje Railsów są błędne w odniesieniu do prawidłowego projektowania relacyjnej bazy danych.

Bill Karwin
źródło
2
Dziękuję za kontakt. Tak więc jesteśmy na tej samej stronie, w asocjacjach polimorficznych Railsów używamy w naszym Komentarzu dwóch kolumn dla klucza obcego. Jedna kolumna zawiera identyfikator wiersza docelowego, a druga kolumna informuje moduł Active Record, w którym modelu znajduje się ten klucz (artykuł, zdjęcie lub wydarzenie). Czy wiedząc o tym, poleciłbyś trzy alternatywy, które zaproponowałeś? Trudno mi się ogarnąć twoją propozycją „Concrete Supertable”. Co masz na myśli, gdy mówisz „połącz swoje komentarze z tym supertable” (komentowalne)?
eggdrop
1
Dziękuję za wyjaśnienie. Myślę, że rozumiem, dlaczego mówisz, że konwencje Railsów są błędne w odniesieniu do prawidłowego projektowania relacyjnej bazy danych - wzorzec pod pewnymi względami przypomina używanie plików płaskich jako mechanizmu przechowywania, ponieważ traci zdolność do wymuszania różnych ograniczeń relacyjnych.
Eggdrop
7
Dokładnie. Powinien być silny "zapach kodu", że nie jest to poprawny projekt relacyjnej bazy danych, kiedy sama dokumentacja Polymorphic Associations mówi, że nie można używać ograniczeń klucza obcego!
Bill Karwin
1
Jedną z wad rozwiązania Concrete Supertable jest to, że nie wymusza spójności referencyjnej na stole podrzędnym. Na przykład byłoby możliwe, aby wiersz Wydarzenia i wiersz Zdjęcia miały ten sam identyfikator commentable_id. To prawda, użycie dobrej procedury tworzenia commentable_id i przypisywania go do tabeli podrzędnej powinno uniknąć takiej sytuacji, ale taka możliwość nadal istnieje.
Jason Martens
1
@Mohamad, STI będzie działać dobrze. Nadal możesz zdefiniować klucze obce, jeśli twoja tabela nadrzędna używa STI. Lub nawet jeśli tabela podrzędna używała STI.
Bill Karwin,
3

Bill Karwin ma rację, mówiąc, że klucze obce nie mogą być używane w relacjach polimorficznych, ponieważ SQL nie ma tak naprawdę natywnej koncepcji relacji polimorficznych. Ale jeśli Twoim celem posiadania klucza obcego jest wymuszenie integralności referencyjnej, możesz go zasymulować za pomocą wyzwalaczy. Jest to specyficzne dla bazy danych, ale poniżej znajduje się kilka ostatnich wyzwalaczy, które utworzyłem, aby zasymulować zachowanie usuwania kaskadowego klucza obcego w relacji polimorficznej:

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();

W moim kodzie rekord w brokeragestabeli lub rekord w agentstabeli może odnosić się do rekordu w subscriberstabeli.

Eric Anderson
źródło
To jest świetne. Jakieś pomysły, jak można stworzyć podobny wyzwalacz, aby upewnić się, że nowo utworzone skojarzenia polimorficzne wskazują na prawidłowy typ i identyfikator?
cayblood